Code drop from //branches/cupcake/...@124589
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java
index 3fe38e8..ea65a46 100644
--- a/src/com/android/browser/AddBookmarkPage.java
+++ b/src/com/android/browser/AddBookmarkPage.java
@@ -114,7 +114,8 @@
      */
     boolean save() {
         String title = mTitle.getText().toString().trim();
-        String unfilteredUrl = mAddress.getText().toString();
+        String unfilteredUrl = 
+                BrowserActivity.fixUrl(mAddress.getText().toString());
         boolean emptyTitle = title.length() == 0;
         boolean emptyUrl = unfilteredUrl.trim().length() == 0;
         Resources r = getResources();
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index fc4acfc..fe27f8d 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -62,6 +62,7 @@
 import android.net.http.RequestQueue;
 import android.net.http.SslCertificate;
 import android.net.http.SslError;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.Debug;
@@ -75,13 +76,16 @@
 import android.os.ServiceManager;
 import android.os.SystemClock;
 import android.os.SystemProperties;
-import android.pim.DateFormat;
+import android.preference.PreferenceManager;
 import android.provider.Browser;
 import android.provider.Checkin;
 import android.provider.Contacts.Intents.Insert;
 import android.provider.Contacts;
 import android.provider.Downloads;
+import android.provider.MediaStore;
 import android.text.IClipboard;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
 import android.text.util.Regex;
 import android.util.Config;
 import android.util.Log;
@@ -94,7 +98,6 @@
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.Window;
@@ -142,6 +145,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.Vector;
@@ -179,6 +183,8 @@
     private void setupHomePage() {
         final Runnable getAccount = new Runnable() {
             public void run() {
+                // Lower priority
+                Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                 // get the default home page
                 String homepage = mSettings.getHomePage();
 
@@ -242,7 +248,9 @@
                 mGls = IGoogleLoginService.Stub.asInterface(service);
                 if (done[0] == false) {
                     done[0] = true;
-                    new Thread(getAccount).start();
+                    Thread account = new Thread(getAccount);
+                    account.setName("GLSAccount");
+                    account.start();
                 }
             }
             public void onServiceDisconnected(ComponentName className) {
@@ -513,6 +521,8 @@
          * as there is a limit of 1Mb (see Asset.h)
          */
         public void run() {
+            // Lower the priority
+            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
             try {
                 if (pluginsPath == null) {
                     Log.e(TAG, "No plugins path found!");
@@ -598,11 +608,24 @@
         if (copyPluginsFromAssets.newSystemImage())  {
           if (copyPluginsFromAssets.checkIsDifferentVersions()) {
             copyPluginsFromAssets.cleanPluginsDirectory();
-            new Thread(copyPluginsFromAssets).start();
+            Thread copyplugins = new Thread(copyPluginsFromAssets);
+            copyplugins.setName("CopyPlugins");
+            copyplugins.start();
           }
         }
     }
 
+    private class ClearThumbnails extends AsyncTask<File, Void, Void> {
+        @Override
+        public Void doInBackground(File... files) {
+            if (files != null) {
+                for (File f : files) {
+                    f.delete();
+                }
+            }
+            return null;
+        }
+    }
 
     @Override public void onCreate(Bundle icicle) {
         if (Config.LOGV) {
@@ -621,6 +644,9 @@
 
         mResolver = getContentResolver();
 
+        setBaseSearchUrl(PreferenceManager.getDefaultSharedPreferences(this)
+                .getString("search_url", ""));
+
         //
         // start MASF proxy service
         //
@@ -658,10 +684,19 @@
         mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser");
 
         if (!mTabControl.restoreState(icicle)) {
+            // clear up the thumbnail directory if we can't restore the state as
+            // none of the files in the directory are referenced any more.
+            new ClearThumbnails().execute(
+                    mTabControl.getThumbnailDir().listFiles());
             final Intent intent = getIntent();
             final Bundle extra = intent.getExtras();
             // Create an initial tab.
-            final TabControl.Tab t = mTabControl.createNewTab();
+            // If the intent is ACTION_VIEW and data is not null, the Browser is
+            // invoked to view the content by another application. In this case,
+            // the tab will be close when exit.
+            final TabControl.Tab t = mTabControl.createNewTab(
+                    Intent.ACTION_VIEW.equals(intent.getAction()) &&
+                    intent.getData() != null);
             mTabControl.setCurrentTab(t);
             // This is one of the only places we call attachTabToContentView
             // without animating from the tab picker.
@@ -722,6 +757,15 @@
 
     @Override
     protected void onNewIntent(Intent intent) {
+        // When a tab is closed on exit, the current tab index is set to -1.
+        // Reset before proceed as Browser requires the current tab to be set.
+        if (mTabControl.getCurrentIndex() == -1) {
+            TabControl.Tab current = mTabControl.getTab(0);
+            mTabControl.setCurrentTab(current);
+            attachTabToContentView(current);
+            mWebView = current.getWebView();
+            resetTitleAndIcon(mWebView);
+        }
         if (mWebView == null) {
             return;
         }
@@ -734,6 +778,7 @@
         }
         if (Intent.ACTION_VIEW.equals(action)
                 || Intent.ACTION_SEARCH.equals(action)
+                || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
                 || Intent.ACTION_WEB_SEARCH.equals(action)) {
             String url = getUrlFromIntent(intent);
             if (url == null || url.length() == 0) {
@@ -742,9 +787,10 @@
             if (Intent.ACTION_VIEW.equals(action) && 
                     (flags & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
                 // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url will be
-                // opened in a new tab unless we have reached MAX_TABS and the
-                // url will be opened in the current tab
-                openTabAndShow(url, null);
+                // opened in a new tab unless we have reached MAX_TABS. Then the
+                // url will be opened in the current tab. If a new tab is
+                // created, it will have "true" for exit on close.
+                openTabAndShow(url, null, true);
             } else {
                 if ("about:debug".equals(url)) {
                     mSettings.toggleDebugSettings();
@@ -779,24 +825,39 @@
                     }
                 }
             } else if (Intent.ACTION_SEARCH.equals(action)
+                    || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
                     || Intent.ACTION_WEB_SEARCH.equals(action)) {
                 url = intent.getStringExtra(SearchManager.QUERY);
-                mLastEnteredUrl = url;
-                // Don't add Urls, just search terms.
-                // Urls will get added when the page is loaded.
-                if (!Regex.WEB_URL_PATTERN.matcher(url).matches()) {
-                    Browser.updateVisitedHistory(mResolver, url, false);
+                if (url != null) {
+                    mLastEnteredUrl = url;
+                    // Don't add Urls, just search terms.
+                    // Urls will get added when the page is loaded.
+                    if (!Regex.WEB_URL_PATTERN.matcher(url).matches()) {
+                        Browser.updateVisitedHistory(mResolver, url, false);
+                    }
+                    // In general, we shouldn't modify URL from Intent.
+                    // But currently, we get the user-typed URL from search box as well.
+                    url = fixUrl(url);
+                    url = smartUrlFilter(url);
+                    String searchSource = "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&";
+                    if (url.contains(searchSource)) {
+                        String source = null;
+                        final Bundle appData = intent.getBundleExtra(SearchManager.APP_DATA);
+                        if (appData != null) {
+                            source = appData.getString(SearchManager.SOURCE);
+                        }
+                        if (TextUtils.isEmpty(source)) {
+                            source = GOOGLE_SEARCH_SOURCE_UNKNOWN;
+                        }
+                        url = url.replace(searchSource, "&source=android-"+source+"&");
+                    }
                 }
-                // In general, we shouldn't modify URL from Intent.
-                // But currently, we get the user-typed URL from search box as well.
-                url = fixUrl(url);
-                url = smartUrlFilter(url);
             }
         }
         return url;
     }
 
-    private String fixUrl(String inUrl) {
+    /* package */ static String fixUrl(String inUrl) {
         if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
             return inUrl;
         if (inUrl.startsWith("http:") ||
@@ -970,7 +1031,7 @@
         }
 
         mActivityInPause = true;
-        if (!pauseWebView()) {
+        if (mTabControl.getCurrentIndex() >= 0 && !pauseWebView()) {
             mWakeLock.acquire();
             mHandler.sendMessageDelayed(mHandler
                     .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
@@ -1057,6 +1118,9 @@
             showHttpAuthentication(mHttpAuthHandler, null, null, title,
                     name, password, focusId);
         }
+        if (mFindDialog != null && mFindDialog.isShowing()) {
+            mFindDialog.onConfigurationChanged(newConfig);
+        }
     }
 
     @Override public void onLowMemory() {
@@ -1192,6 +1256,11 @@
         // options selector, so set mCanChord to true so we can access them.
         mCanChord = true;
         int id = item.getItemId();
+        final WebView webView = getTopWindow();
+        final HashMap hrefMap = new HashMap();
+        hrefMap.put("webview", webView);
+        final Message msg = mHandler.obtainMessage(
+                FOCUS_NODE_HREF, id, 0, hrefMap);
         switch (id) {
             // -- Browser context menu
             case R.id.open_context_menu_id:
@@ -1200,21 +1269,9 @@
             case R.id.save_link_context_menu_id:
             case R.id.share_link_context_menu_id:
             case R.id.copy_link_context_menu_id:
-                Message msg = mHandler.obtainMessage(
-                        FOCUS_NODE_HREF, id, 0);
-                WebView webview = getTopWindow();
-                msg.obj = webview;
-                webview.requestFocusNodeHref(msg);
+                webView.requestFocusNodeHref(msg);
                 break;
 
-            case R.id.download_context_menu_id:
-            case R.id.view_image_context_menu_id:
-                Message m = mHandler.obtainMessage(
-                        FOCUS_NODE_HREF, id, 0);
-                WebView w = getTopWindow();
-                m.obj = w;
-                w.requestImageRef(m);
-                break;
             default:
                 // For other context menus
                 return onOptionsItemSelected(item);
@@ -1222,18 +1279,34 @@
         mCanChord = false;
         return true;
     }
-    
+
+    private Bundle createGoogleSearchSourceBundle(String source) {
+        Bundle bundle = new Bundle();
+        bundle.putString(SearchManager.SOURCE, source);
+        return bundle;
+    }
+
     /**
      * Overriding this forces the search key to launch global search.  The difference
      * is the final "true" which requests global search.
      */
     @Override
     public boolean onSearchRequested() {
-        startSearch(null, false, null, true); 
+        startSearch(null, false,
+                createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHKEY), true);
         return true;
     }
 
     @Override
+    public void startSearch(String initialQuery, boolean selectInitialQuery, 
+            Bundle appSearchData, boolean globalSearch) {
+        if (appSearchData == null) {
+            appSearchData = createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_TYPE);
+        }
+        super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
+    }
+
+    @Override
     public boolean onOptionsItemSelected(MenuItem item) {
         if (!mCanChord) {
             // The user has already fired a shortcut with this hold down of the
@@ -1244,23 +1317,27 @@
             // -- Main menu
             case R.id.goto_menu_id: {
                 String url = getTopWindow().getUrl();
-                // TODO: Activities are requested to call onSearchRequested, and to override
-                // that function in order to insert custom fields (e.g. the search query).
-                startSearch(mSettings.getHomePage().equals(url) ? null : url, true, null, false);
+                startSearch(mSettings.getHomePage().equals(url) ? null : url, true,
+                        createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_GOTO), false);
                 }
                 break;
-                
+
             case R.id.search_menu_id:
                 // launch using "global" search, which will bring up the Google search box
-                onSearchRequested(); 
+                startSearch(null, false,
+                        createGoogleSearchSourceBundle(GOOGLE_SEARCH_SOURCE_SEARCHMENU), true);
                 break;
-        
+
             case R.id.bookmarks_menu_id:
                 bookmarksPicker();
                 break;
 
             case R.id.windows_menu_id:
-                tabPicker(true, mTabControl.getCurrentIndex(), false);
+                if (mTabControl.getTabCount() == 1) {
+                    openTabAndShow(mSettings.getHomePage(), null, false);
+                } else {
+                    tabPicker(true, mTabControl.getCurrentIndex(), false);
+                }
                 break;
 
             case R.id.stop_reload_menu_id:
@@ -1299,7 +1376,7 @@
                         indexToShow--;
                     }
                 }
-                removeTabAndShow(currentIndex, indexToShow);
+                switchTabs(currentIndex, indexToShow, true);
                 break;
 
             case R.id.homepage_menu_id:
@@ -1313,39 +1390,21 @@
                 startActivityForResult(intent, PREFERENCES_PAGE);
                 break;
 
-/*
-            Disable Find for version 1.0
             case R.id.find_menu_id:
                 if (null == mFindDialog) {
                     mFindDialog = new FindDialog(this);
-                    FrameLayout.LayoutParams lp = 
-                        new FrameLayout.LayoutParams(
-                        ViewGroup.LayoutParams.FILL_PARENT, 
-                        ViewGroup.LayoutParams.WRAP_CONTENT, 
-                        Gravity.BOTTOM);
-                        mFindDialog.setLayoutParams(lp);
                 }
                 mFindDialog.setWebView(getTopWindow());
-                mContentView.addView(mFindDialog);
                 mFindDialog.show();
-                Animation anim =AnimationUtils.loadAnimation(this,
-                        R.anim.find_dialog_enter);
-                mFindDialog.startAnimation(anim);
                 mMenuState = EMPTY_MENU;
                 break;
-*/
 
             case R.id.page_info_menu_id:
-                showPageInfo(mWebView, false);
+                showPageInfo(mTabControl.getCurrentTab(), false);
                 break;
 
-            case R.id.classic_history_menu_id: {
-                    Intent i = new Intent(this, BrowserHistoryPage.class);
-                    i.putExtra("maxTabsOpen",
-                            mTabControl.getTabCount() >=
-                            TabControl.MAX_TABS);
-                    startActivityForResult(i, CLASSIC_HISTORY_PAGE);
-                }
+            case R.id.classic_history_menu_id:
+                loadHistory();
                 break;
 
             case R.id.bookmark_page_menu_id:
@@ -1448,15 +1507,33 @@
             case R.id.properties_tab_menu_id:
                 if (mTabListener != null && mTabOverview != null) {
                     int pos = mTabOverview.getContextMenuPosition(item);
-                    TabControl.Tab t = mTabControl.getTab(pos);
-                    // Use the tab's data for the page info dialog.
-                    if (t.getWebView() != null) {
-                        showPageInfo(t.getWebView(), false);
-                    }
-                    // FIXME: what should we display if the WebView is null?
+                    showPageInfo(mTabControl.getTab(pos), false);
                 }
                 break;
 
+            case R.id.window_one_menu_id:
+            case R.id.window_two_menu_id:
+            case R.id.window_three_menu_id:
+            case R.id.window_four_menu_id:
+            case R.id.window_five_menu_id:
+            case R.id.window_six_menu_id:
+            case R.id.window_seven_menu_id:
+            case R.id.window_eight_menu_id:
+                {
+                    int menuid = item.getItemId();
+                    for (int id = 0; id < WINDOW_SHORTCUT_ID_ARRAY.length; id++) {
+                        if (WINDOW_SHORTCUT_ID_ARRAY[id] == menuid) {
+                            TabControl.Tab desiredTab = mTabControl.getTab(id);
+                            if (desiredTab != null && 
+                                    desiredTab != mTabControl.getCurrentTab()) {
+                                switchTabs(mTabControl.getCurrentIndex(), id, false);
+                            }
+                            break;
+                        } 
+                    }
+                }
+                break;
+                
             default:
                 if (!super.onOptionsItemSelected(item)) {
                     return false;
@@ -1468,29 +1545,9 @@
     }
 
     public void closeFind() {
-        Animation anim = AnimationUtils.loadAnimation(this,
-                R.anim.find_dialog_exit);
-        mFindDialog.startAnimation(anim);
-        mContentView.removeView(mFindDialog);
-        getTopWindow().requestFocus();
         mMenuState = R.id.MAIN_MENU;
     }
 
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent event) {
-        if (super.dispatchTouchEvent(event)) {
-            return true;
-        } else {
-            // We do not use the Dialog class because it places dialogs in the
-            // middle of the screen.  It would take care of dismissing find if
-            // were using it, but we are doing it manually since we are not.
-            if (mFindDialog != null && mFindDialog.hasFocus()) {
-                mFindDialog.dismiss();
-            }
-            return false;
-        }
-    }
-    
     @Override public boolean onPrepareOptionsMenu(Menu menu)
     {
         // This happens when the user begins to hold down the menu key, so
@@ -1533,14 +1590,11 @@
                 final MenuItem back = menu.findItem(R.id.back_menu_id);
                 back.setVisible(canGoBack);
                 back.setEnabled(canGoBack);
-                final MenuItem close = menu.findItem(R.id.close_menu_id);
-                close.setVisible(!canGoBack);
-                close.setEnabled(!canGoBack);
                 final MenuItem flip = 
                         menu.findItem(R.id.flip_orientation_menu_id);
                 boolean keyboardClosed = 
-                        getResources().getConfiguration().keyboardHidden == 
-                        Configuration.KEYBOARDHIDDEN_YES;
+                        getResources().getConfiguration().hardKeyboardHidden == 
+                        Configuration.HARDKEYBOARDHIDDEN_YES;
                 flip.setEnabled(keyboardClosed);
 
                 boolean isHome = mSettings.getHomePage().equals(w.getUrl());
@@ -1562,6 +1616,19 @@
                         PackageManager.MATCH_DEFAULT_ONLY);
                 menu.findItem(R.id.share_page_menu_id).setVisible(
                         list.size() > 0);
+                
+                // Hide the menu+<window number> items
+                // Can't set visibility in menu xml file b/c when a
+                // group is set visible, all items are set visible.
+                for (int i = 0; i < WINDOW_SHORTCUT_ID_ARRAY.length; i++) {
+                    menu.findItem(WINDOW_SHORTCUT_ID_ARRAY[i]).setVisible(false);
+                }
+
+                // If there is only 1 window, the text will be "New window"
+                final MenuItem windows = menu.findItem(R.id.windows_menu_id);
+                windows.setTitleCondensed(mTabControl.getTabCount() > 1 ?
+                        getString(R.string.view_tabs_condensed) :
+                        getString(R.string.tab_picker_new_tab));
 
                 boolean isNavDump = mSettings.isNavDump();
                 final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id);
@@ -1608,26 +1675,22 @@
         menu.setGroupVisible(R.id.GEO_MENU,
                 type == WebView.HitTestResult.GEO_TYPE);
         menu.setGroupVisible(R.id.IMAGE_MENU,
-                type == WebView.HitTestResult.IMAGE_TYPE ||
-                type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE
+                type == WebView.HitTestResult.IMAGE_TYPE
                 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
         menu.setGroupVisible(R.id.ANCHOR_MENU,
-                type == WebView.HitTestResult.ANCHOR_TYPE ||
-                type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE
-                || type == WebView.HitTestResult.SRC_ANCHOR_TYPE
+                type == WebView.HitTestResult.SRC_ANCHOR_TYPE
                 || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
 
         // Setup custom handling depending on the type
         switch (type) {
             case WebView.HitTestResult.PHONE_TYPE:
-                menu.setHeaderTitle(extra);
+                menu.setHeaderTitle(Uri.decode(extra));
                 menu.findItem(R.id.dial_context_menu_id).setIntent(
                         new Intent(Intent.ACTION_VIEW, Uri
                                 .parse(WebView.SCHEME_TEL + extra)));
-                Intent addIntent = new Intent(Intent.ACTION_INSERT,
-                        Contacts.People.CONTENT_URI);
-                addIntent.putExtra(Insert.FULL_MODE, true);
-                addIntent.putExtra(Insert.PHONE, extra);
+                Intent addIntent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+                addIntent.putExtra(Insert.PHONE, Uri.decode(extra));
+                addIntent.setType(Contacts.People.CONTENT_ITEM_TYPE);
                 menu.findItem(R.id.add_contact_context_menu_id).setIntent(
                         addIntent);
                 menu.findItem(R.id.copy_phone_context_menu_id).setOnMenuItemClickListener(
@@ -1653,35 +1716,16 @@
                         new Copy(extra));
                 break;
 
-            case WebView.HitTestResult.ANCHOR_TYPE:
-            case WebView.HitTestResult.IMAGE_ANCHOR_TYPE:
             case WebView.HitTestResult.SRC_ANCHOR_TYPE:
             case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
-                mTitleView = (TextView) LayoutInflater.from(this)
+                TextView titleView = (TextView) LayoutInflater.from(this)
                         .inflate(android.R.layout.browser_link_context_header,
                         null);
-                menu.setHeaderView(mTitleView);
+                titleView.setText(extra);
+                menu.setHeaderView(titleView);
                 // decide whether to show the open link in new tab option
                 menu.findItem(R.id.open_newtab_context_menu_id).setVisible(
                         mTabControl.getTabCount() < TabControl.MAX_TABS);
-                if (type == WebView.HitTestResult.ANCHOR_TYPE
-                        || type == WebView.HitTestResult.IMAGE_ANCHOR_TYPE){
-                    menu.findItem(R.id.bookmark_context_menu_id).setVisible(
-                            false);
-                    menu.findItem(R.id.save_link_context_menu_id).setVisible(
-                            false);
-                    menu.findItem(R.id.copy_link_context_menu_id).setVisible(
-                            false);
-                    menu.findItem(R.id.share_link_context_menu_id).setVisible(
-                            false);
-                    mTitleView.setText(R.string.contextmenu_javascript);
-                    break;
-                }
-                Message headerMessage = mHandler.obtainMessage(FOCUS_NODE_HREF,
-                        HEADER_FLAG, 0);
-                headerMessage.obj = webview;
-                webview.requestFocusNodeHref(headerMessage);
-                // decide whether to show the share link option
                 PackageManager pm = getPackageManager();
                 Intent send = new Intent(Intent.ACTION_SEND);
                 send.setType("text/plain");
@@ -1689,12 +1733,14 @@
                         PackageManager.MATCH_DEFAULT_ONLY);
                 menu.findItem(R.id.share_link_context_menu_id).setVisible(
                         list.size() > 0);
-                if (type == WebView.HitTestResult.ANCHOR_TYPE) {
-                    break;
-                }
-                //fall through
+                break;
 
             case WebView.HitTestResult.IMAGE_TYPE:
+                menu.setHeaderTitle(extra);
+                menu.findItem(R.id.view_image_context_menu_id).setIntent(
+                        new Intent(Intent.ACTION_VIEW, Uri.parse(extra)));
+                menu.findItem(R.id.download_context_menu_id).
+                        setOnMenuItemClickListener(new Download(extra));
                 break;
 
             default:
@@ -1793,10 +1839,10 @@
         // Increment the count to indicate that we are in an animation.
         mAnimationCount++;
         // Remove the listener so we don't get any more tab changes.
-        if (mTabOverview != null) {
-            mTabOverview.setListener(null);
-        }
+        mTabOverview.setListener(null);
         mTabListener = null;
+        // Make the menu empty until the animation completes.
+        mMenuState = EMPTY_MENU;
 
     }
 
@@ -1828,7 +1874,8 @@
     // the given Message. If the tab overview is already showing (i.e. this
     // method is called from TabListener.onClick(), the method will animate
     // away from the tab overview.
-    private void openTabAndShow(String url, final Message msg) {
+    private void openTabAndShow(String url, final Message msg,
+            boolean closeOnExit) {
         final boolean newTab = mTabControl.getTabCount() != TabControl.MAX_TABS;
         final TabControl.Tab currentTab = mTabControl.getCurrentTab();
         if (newTab) {
@@ -1856,7 +1903,7 @@
                 }
                 // Animate from the Tab overview after any animations have
                 // finished.
-                sendAnimateFromOverview(mTabControl.createNewTab(),
+                sendAnimateFromOverview(mTabControl.createNewTab(closeOnExit),
                         true, url, delay, msg);
             }
         } else if (url != null) {
@@ -1900,10 +1947,6 @@
     // be displayed to the user.
     private void animateToTabOverview(final int newIndex, final boolean remove,
             final AnimatingView view) {
-        if (mTabOverview == null) {
-            return;
-        }
-
         // Find the view in the ImageGrid allowing for the "New Tab" cell.
         int position = mTabControl.getTabIndex(view.mTab);
         if (!((ImageAdapter) mTabOverview.getAdapter()).maxedOut()) {
@@ -1920,12 +1963,10 @@
         final Animation.AnimationListener l =
                 new Animation.AnimationListener() {
                     public void onAnimationStart(Animation a) {
-                        if (mTabOverview != null) {
-                            mTabOverview.requestFocus();
-                            // Clear the listener so we don't trigger a tab
-                            // selection.
-                            mTabOverview.setListener(null);
-                        }
+                        mTabOverview.requestFocus();
+                        // Clear the listener so we don't trigger a tab
+                        // selection.
+                        mTabOverview.setListener(null);
                     }
                     public void onAnimationRepeat(Animation a) {}
                     public void onAnimationEnd(Animation a) {
@@ -1940,17 +1981,15 @@
                             public void run() {
                                 // Remove the AnimatingView.
                                 mContentView.removeView(view);
-                                if (mTabOverview != null) {
-                                    // Make newIndex visible.
-                                    mTabOverview.setCurrentIndex(newIndex);
-                                    // Restore the listener.
-                                    mTabOverview.setListener(mTabListener);
-                                    // Change the menu to TAB_MENU if the
-                                    // ImageGrid is interactive.
-                                    if (mTabOverview.isLive()) {
-                                        mMenuState = R.id.TAB_MENU;
-                                        mTabOverview.requestFocus();
-                                    }
+                                // Make newIndex visible.
+                                mTabOverview.setCurrentIndex(newIndex);
+                                // Restore the listener.
+                                mTabOverview.setListener(mTabListener);
+                                // Change the menu to TAB_MENU if the
+                                // ImageGrid is interactive.
+                                if (mTabOverview.isLive()) {
+                                    mMenuState = R.id.TAB_MENU;
+                                    mTabOverview.requestFocus();
                                 }
                                 // If a remove was requested, remove the tab.
                                 if (remove) {
@@ -1968,12 +2007,10 @@
                                     if (currentTab != tab) {
                                         mTabControl.setCurrentTab(currentTab);
                                     }
-                                    if (mTabOverview != null) {
-                                        mTabOverview.remove(newIndex);
-                                        // Make the current tab visible.
-                                        mTabOverview.setCurrentIndex(
-                                                mTabControl.getCurrentIndex());
-                                    }
+                                    mTabOverview.remove(newIndex);
+                                    // Make the current tab visible.
+                                    mTabOverview.setCurrentIndex(
+                                            mTabControl.getCurrentIndex());
                                 }
                             }
                         });
@@ -1999,11 +2036,6 @@
     // from.
     private void animateFromTabOverview(final AnimatingView view,
             final boolean newTab, final String url, final Message msg) {
-        // mTabOverview may have been dismissed
-        if (mTabOverview == null) {
-            return;
-        }
-
         // firstVisible is the first visible tab on the screen.  This helps
         // to know which corner of the screen the selected tab is.
         int firstVisible = mTabOverview.getFirstVisiblePosition();
@@ -2025,32 +2057,19 @@
         // Find the view at this location.
         final View v = mTabOverview.getChildAt(location);
 
-        // Use a delay of 1 second in case we get a bad position
-        long delay = 1000;
-        boolean fade = false;
-
         // Wait until the animation completes to load the url.
         final Animation.AnimationListener l =
                 new Animation.AnimationListener() {
                     public void onAnimationStart(Animation a) {}
                     public void onAnimationRepeat(Animation a) {}
                     public void onAnimationEnd(Animation a) {
-                        // The animation is done so allow key events and other
-                        // animations to begin.
-                        mAnimationCount--;
                         mHandler.post(new Runnable() {
                             public void run() {
-                                if (v != null) {
-                                    mContentView.removeView(view);
-                                    mWebView.setVisibility(View.VISIBLE);
-                                    // Make the sub window container visible if
-                                    // there is one.
-                                    if (mTabControl.getCurrentSubWindow() != null) {
-                                        mTabControl.getCurrentTab()
-                                                .getSubWebViewContainer()
-                                                .setVisibility(View.VISIBLE);
-                                    }
-                                }
+                                mContentView.removeView(view);
+                                // Dismiss the tab overview. If the cell at the
+                                // given location is null, set the fade
+                                // parameter to true.
+                                dismissTabOverview(v == null);
                                 if (url != null) {
                                     // Dismiss the subwindow if one exists.
                                     dismissSubWindow(
@@ -2065,6 +2084,12 @@
                                 if (msg != null) {
                                     msg.sendToTarget();
                                 }
+                                // The animation is done and the tab overview is
+                                // gone so allow key events and other animations
+                                // to begin.
+                                mAnimationCount--;
+                                // Reset all the title bar info.
+                                resetTitle();
                             }
                         });
                     }
@@ -2077,32 +2102,42 @@
             view.startAnimation(anim);
             // Make the view VISIBLE during the animation.
             view.setVisibility(View.VISIBLE);
-            // Dismiss the tab overview after the animation completes.
-            delay = anim.getDuration();
         } else {
-            // dismiss mTabOverview and have it fade out just in case we get a
-            // bad location.
-            fade = true;
             // Go ahead and load the url.
             l.onAnimationEnd(null);
         }
-        // Reset all the title bar info.
-        resetTitle();
-        // Dismiss the tab overview either after the animation or after a
-        // second.
-        mHandler.sendMessageDelayed(mHandler.obtainMessage(
-                DISMISS_TAB_OVERVIEW, fade ? 1 : 0, 0), delay);
+    }
+
+    // Dismiss the tab overview applying a fade if needed.
+    private void dismissTabOverview(final boolean fade) {
+        if (fade) {
+            AlphaAnimation anim = new AlphaAnimation(1.0f, 0.0f);
+            anim.setDuration(500);
+            anim.startNow();
+            mTabOverview.startAnimation(anim);
+        }
+        // Just in case there was a problem with animating away from the tab
+        // overview
+        mWebView.setVisibility(View.VISIBLE);
+        // Make the sub window container visible.
+        if (mTabControl.getCurrentSubWindow() != null) {
+            mTabControl.getCurrentTab().getSubWebViewContainer()
+                    .setVisibility(View.VISIBLE);
+        }
+        mContentView.removeView(mTabOverview);
+        mTabOverview.clear();
+        mTabOverview = null;
+        mTabListener = null;
     }
 
     private void openTab(String url) {
         if (mSettings.openInBackground()) {
-            TabControl.Tab t = mTabControl.createNewTab();
+            TabControl.Tab t = mTabControl.createNewTab(false);
             if (t != null) {
-                WebView w = t.getWebView();
-                w.loadUrl(url);
+                t.getWebView().loadUrl(url);
             }
         } else {
-            openTabAndShow(url, null);
+            openTabAndShow(url, null, false);
         }
     }
 
@@ -2118,6 +2153,19 @@
             mText = toCopy;
         }
     }
+    
+    private class Download implements OnMenuItemClickListener {
+        private String mText;
+
+        public boolean onMenuItemClick(MenuItem item) {
+            onDownloadStartNoStream(mText, null, null, null, -1);
+            return true;
+        }
+
+        public Download(String toDownload) {
+            mText = toDownload;
+        }
+    }
 
     private void copy(CharSequence text) {
         try {
@@ -2184,7 +2232,11 @@
         mUrl = url;
         mTitle = title;
 
-        setTitle(buildUrlTitle(url, title));
+        // While the tab overview is animating or being shown, block changes
+        // to the title.
+        if (mAnimationCount == 0 && mTabOverview == null) {
+            setTitle(buildUrlTitle(url, title));
+        }
     }
 
     /**
@@ -2255,6 +2307,11 @@
 
     // Set the favicon in the title bar.
     private void setFavicon(Bitmap icon) {
+        // While the tab overview is animating or being shown, block changes to
+        // the favicon.
+        if (mAnimationCount > 0 || mTabOverview != null) {
+            return;
+        }
         Drawable[] array = new Drawable[2];
         PaintDrawable p = new PaintDrawable(Color.WHITE);
         p.setCornerRadius(3f);
@@ -2294,11 +2351,11 @@
         updateLockIconImage(mLockIconType);
     }
 
-    private void removeTabAndShow(int indexToRemove, int indexToShow) {
+    private void switchTabs(int indexFrom, int indexToShow, boolean remove) {
         int delay = TAB_ANIMATION_DURATION + TAB_OVERVIEW_DELAY;
         // Animate to the tab picker, remove the current tab, then
         // animate away from the tab picker to the parent WebView.
-        tabPicker(false, indexToRemove, true);
+        tabPicker(false, indexFrom, remove);
         // Change to the parent tab
         final TabControl.Tab tab = mTabControl.getTab(indexToShow);
         if (tab != null) {
@@ -2315,13 +2372,25 @@
         if (mWebView.canGoBack()) {
             mWebView.goBack();
         } else {
+            TabControl.Tab self = mTabControl.getCurrentTab();
             // Check to see if we are closing a window that was created by
             // another window. If so, we switch back to that window.
-            TabControl.Tab parent = mTabControl.getCurrentTab().getParentTab();
+            TabControl.Tab parent = self.getParentTab();
             if (parent != null) {
-                removeTabAndShow(mTabControl.getCurrentIndex(),
-                        mTabControl.getTabIndex(parent));
+                switchTabs(mTabControl.getCurrentIndex(),
+                        mTabControl.getTabIndex(parent), true);
             } else {
+                if (self.closeOnExit()) {
+                    if (mTabControl.getTabCount() == 1) {
+                        finish();
+                        return;
+                    }
+                    // call pauseWebView() now, we won't be able to call it in
+                    // onPause() as the mWebView won't be valid.
+                    pauseWebView();
+                    removeTabFromContentView(self);
+                    mTabControl.removeTab(self);
+                }
                 /*
                  * Instead of finishing the activity, simply push this to the back
                  * of the stack and let ActivityManager to choose the foreground
@@ -2352,7 +2421,10 @@
             if (mAnimationCount > 0) {
                 return KeyTracker.State.DONE_TRACKING;
             }
-            if (stage == KeyTracker.Stage.UP) {
+            if (stage == KeyTracker.Stage.LONG_REPEAT) {
+                loadHistory();
+                return KeyTracker.State.DONE_TRACKING;
+            } else if (stage == KeyTracker.Stage.UP) {
                 // FIXME: Currently, we do not have a notion of the
                 // history picker for the subwindow, but maybe we
                 // should?
@@ -2381,14 +2453,12 @@
         if (!handled) {
             switch (keyCode) {
                 case KeyEvent.KEYCODE_SPACE:
-                    if (mMenuState == R.id.MAIN_MENU){
-                        if (event.isShiftPressed()) {
-                            getTopWindow().pageUp(false);
-                        } else {
-                            getTopWindow().pageDown(false);
-                        }
-                        handled = true;
+                    if (event.isShiftPressed()) {
+                        getTopWindow().pageUp(false);
+                    } else {
+                        getTopWindow().pageDown(false);
                     }
+                    handled = true;
                     break;
 
                 default:
@@ -2424,6 +2494,13 @@
         }
     }
 
+    private void loadHistory() {
+        Intent intent = new Intent(this, BrowserHistoryPage.class);
+        intent.putExtra("maxTabsOpen",
+                mTabControl.getTabCount() >= TabControl.MAX_TABS);
+        startActivityForResult(intent, CLASSIC_HISTORY_PAGE);
+    }
+
     // called by a non-UI thread to post the message
     public void postMessage(int what, int arg1, int arg2, Object obj) {
         mHandler.sendMessage(mHandler.obtainMessage(what, arg1, arg2, obj));
@@ -2436,13 +2513,12 @@
     // Message Ids
     private static final int JS_CONFIRM              = 101;
     private static final int FOCUS_NODE_HREF         = 102;
-    private static final int DISMISS_TAB_OVERVIEW    = 103;
-    private static final int CANCEL_CREDS_REQUEST    = 104;
-    private static final int ANIMATE_FROM_OVERVIEW   = 105;
-    private static final int ANIMATE_TO_OVERVIEW     = 106;
-    private static final int OPEN_TAB_AND_SHOW       = 107;
-    private static final int CHECK_MEMORY            = 108;
-    private static final int RELEASE_WAKELOCK        = 109;
+    private static final int CANCEL_CREDS_REQUEST    = 103;
+    private static final int ANIMATE_FROM_OVERVIEW   = 104;
+    private static final int ANIMATE_TO_OVERVIEW     = 105;
+    private static final int OPEN_TAB_AND_SHOW       = 106;
+    private static final int CHECK_MEMORY            = 107;
+    private static final int RELEASE_WAKELOCK        = 108;
 
     // Private handler for handling javascript and saving passwords
     private Handler mHandler = new Handler() {
@@ -2458,37 +2534,6 @@
                     }
                     break;
 
-                case DISMISS_TAB_OVERVIEW:
-                    if (mTabOverview != null) {
-                        if (msg.arg1 == 1) {
-                            AlphaAnimation anim =
-                                    new AlphaAnimation(1.0f, 0.0f);
-                            anim.setDuration(500);
-                            anim.startNow();
-                            mTabOverview.startAnimation(anim);
-                        }
-                        // Just in case there was a problem with animating away
-                        // from the tab overview
-                        mWebView.setVisibility(View.VISIBLE);
-                        // Make the sub window container visible.
-                        if (mTabControl.getCurrentSubWindow() != null) {
-                            mTabControl.getCurrentTab().getSubWebViewContainer()
-                                    .setVisibility(View.VISIBLE);
-                        }
-                        mContentView.removeView(mTabOverview);
-                        mTabOverview.clear();
-                        // XXX: There are checks for mTabOverview throughout
-                        // this file because this message can be received
-                        // before it is expected. This is because we are not
-                        // enforcing the order of animations properly. In order
-                        // to get this right, we would need to rewrite a lot of
-                        // the code to dispatch this messages after all
-                        // animations have completed.
-                        mTabOverview = null;
-                        mTabListener = null;
-                    }
-                    break;
-                    
                 case ANIMATE_FROM_OVERVIEW:
                     final HashMap map = (HashMap) msg.obj;
                     animateFromTabOverview((AnimatingView) map.get("view"),
@@ -2502,7 +2547,7 @@
                     break;
 
                 case OPEN_TAB_AND_SHOW:
-                    openTabAndShow((String) msg.obj, null);
+                    openTabAndShow((String) msg.obj, null, false);
                     break;
 
                 case FOCUS_NODE_HREF:
@@ -2510,18 +2555,16 @@
                     if (url == null || url.length() == 0) {
                         break;
                     }
-                    WebView view = (WebView) msg.obj;
+                    HashMap focusNodeMap = (HashMap) msg.obj;
+                    WebView view = (WebView) focusNodeMap.get("webview");
                     // Only apply the action if the top window did not change.
                     if (getTopWindow() != view) {
                         break;
                     }
                     switch (msg.arg1) {
-                        case HEADER_FLAG:
-                            mTitleView.setText(url);
-                            break;
                         case R.id.open_context_menu_id:
                         case R.id.view_image_context_menu_id:
-                            loadURL(url);
+                            loadURL(getTopWindow(), url);
                             break;
                         case R.id.open_newtab_context_menu_id:
                             openTab(url);
@@ -2546,7 +2589,7 @@
                     break;
 
                 case LOAD_URL:
-                    loadURL((String) msg.obj);
+                    loadURL(getTopWindow(), (String) msg.obj);
                     break;
 
                 case STOP_LOAD:
@@ -2574,9 +2617,6 @@
         }
     };
 
-    private static final int HEADER_FLAG = Integer.MIN_VALUE;
-    private TextView mTitleView = null;
-
     // -------------------------------------------------------------------------
     // WebViewClient implementation.
     //-------------------------------------------------------------------------
@@ -2591,14 +2631,22 @@
         return mWebViewClient;
     }
 
+    private void updateIcon(String url, Bitmap icon) {
+        if (icon != null) {
+            BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver,
+                    url, icon);
+        }
+        setFavicon(icon);
+    }
+
     private final WebViewClient mWebViewClient = new WebViewClient() {
         @Override
         public void onPageStarted(WebView view, String url, Bitmap favicon) {
             resetLockIcon(url);
             setUrlTitle(url, null);
-            // Call onReceivedIcon instead of setFavicon so the bookmark
+            // Call updateIcon instead of setFavicon so the bookmark
             // database can be updated.
-            mWebChromeClient.onReceivedIcon(view, favicon);
+            updateIcon(url, favicon);
 
             if (mSettings.isTracing() == true) {
                 // FIXME: we should save the trace file somewhere other than data.
@@ -2664,8 +2712,6 @@
             // Reset the title and icon in case we stopped a provisional
             // load.
             resetTitleAndIcon(view);
-            // Make the progress full.
-            getWindow().setFeatureInt(Window.FEATURE_PROGRESS, 10000);
 
             // Update the lock icon image only once we are done loading
             updateLockIconImage(mLockIconType);
@@ -2891,6 +2937,7 @@
             if (errorCode != EventHandler.ERROR_LOOKUP &&
                     errorCode != EventHandler.ERROR_CONNECT &&
                     errorCode != EventHandler.ERROR_BAD_URL &&
+                    errorCode != EventHandler.ERROR_UNSUPPORTED_SCHEME &&
                     errorCode != EventHandler.FILE_ERROR) {
                 new AlertDialog.Builder(BrowserActivity.this)
                         .setTitle((errorCode == EventHandler.FILE_NOT_FOUND_ERROR) ?
@@ -3106,7 +3153,7 @@
                 // openTabAndShow will dispatch the message after creating the
                 // new WebView. This will prevent another request from coming
                 // in during the animation.
-                openTabAndShow(null, msg);
+                openTabAndShow(null, msg, false);
                 parent.addChildTab(mTabControl.getCurrentTab());
                 WebView.WebViewTransport transport = 
                     (WebView.WebViewTransport) msg.obj;
@@ -3204,13 +3251,18 @@
                     mTabControl.getCurrentTab().getParentTab();
             if (parent != null) {
                 // JavaScript can only close popup window.
-                removeTabAndShow(currentIndex, mTabControl.getTabIndex(parent));
+                switchTabs(currentIndex, mTabControl.getTabIndex(parent), true);
             }
         }
 
         @Override
         public void onProgressChanged(WebView view, int newProgress) {
-            getWindow().setFeatureInt(Window.FEATURE_PROGRESS, newProgress*100);
+            // Block progress updates to the title bar while the tab overview
+            // is animating or being displayed.
+            if (mAnimationCount == 0 && mTabOverview == null) {
+                getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+                        newProgress * 100);
+            }
 
             if (newProgress == 100) {
                 // onProgressChanged() is called for sub-frame too while
@@ -3222,7 +3274,7 @@
 
         @Override
         public void onReceivedTitle(WebView view, String title) {
-            String url = view.getUrl();
+            String url = view.getOriginalUrl();
 
             // here, if url is null, we want to reset the title
             setUrlTitle(url, title);
@@ -3245,7 +3297,9 @@
                 Cursor c = mResolver.query(Browser.BOOKMARKS_URI,
                     Browser.HISTORY_PROJECTION, where, selArgs, null);
                 if (c.moveToFirst()) {
-                    Log.d(LOGTAG, "updating cursor");
+                    if (Config.LOGV) {
+                        Log.v(LOGTAG, "updating cursor");
+                    }
                     // Current implementation of database only has one entry per
                     // url.
                     int titleIndex =
@@ -3263,11 +3317,7 @@
 
         @Override
         public void onReceivedIcon(WebView view, Bitmap icon) {
-            if (icon != null) {
-                BrowserBookmarksAdapter.updateBookmarkFavicon(mResolver,
-                        view.getUrl(), icon);
-            }
-            setFavicon(icon);
+            updateIcon(view.getUrl(), icon);
         }
 
         //----------------------------------------------------------------------
@@ -3470,20 +3520,29 @@
      * @param mimetype The mimetype of the content reported by the server
      * @param contentLength The file size reported by the server
      */
-    public void onDownloadStartNoStream(String url, String userAgent,
+    /*package */ void onDownloadStartNoStream(String url, String userAgent,
             String contentDisposition, String mimetype, long contentLength) {
 
         String filename = URLUtil.guessFileName(url,
                 contentDisposition, mimetype);
 
         // Check to see if we have an SDCard
-        if (!Environment.getExternalStorageState().
-                equals(Environment.MEDIA_MOUNTED)) {
-            String msg =
-                getString(R.string.download_no_sdcard_dlg_msg, filename);
+        String status = Environment.getExternalStorageState();
+        if (!status.equals(Environment.MEDIA_MOUNTED)) {
+            int title;
+            String msg;
+
+            // Check to see if the SDCard is busy, same as the music app
+            if (status.equals(Environment.MEDIA_SHARED)) {
+                msg = getString(R.string.download_sdcard_busy_dlg_msg);
+                title = R.string.download_sdcard_busy_dlg_title;
+            } else {
+                msg = getString(R.string.download_no_sdcard_dlg_msg, filename);
+                title = R.string.download_no_sdcard_dlg_title;
+            }
 
             new AlertDialog.Builder(this)
-                .setTitle(R.string.download_no_sdcard_dlg_title)
+                .setTitle(title)
                 .setIcon(R.drawable.ic_dialog_alert)
                 .setMessage(msg)
                 .setPositiveButton(R.string.ok, null)
@@ -3508,9 +3567,15 @@
         if (contentLength > 0) {
             values.put(Downloads.TOTAL_BYTES, contentLength);
         }
-        final Uri contentUri =
-                getContentResolver().insert(Downloads.CONTENT_URI, values);
-        viewDownloads(contentUri);
+        if (mimetype == null) {
+            // We must have long pressed on a link or image to download it. We
+            // are not sure of the mimetype in this case, so do a head request
+            new FetchUrlMimeType(this).execute(values);
+        } else {
+            final Uri contentUri =
+                    getContentResolver().insert(Downloads.CONTENT_URI, values);
+            viewDownloads(contentUri);
+        }
 
     }
 
@@ -3563,29 +3628,38 @@
         } else if (lockIconType == LOCK_ICON_MIXED) {
             d = mMixLockIcon;
         }
-        getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d);
+        // If the tab overview is animating or being shown, do not update the
+        // lock icon.
+        if (mAnimationCount == 0 && mTabOverview == null) {
+            getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, d);
+        }
     }
 
     /**
      * Displays a page-info dialog.
-     * @param view The target web-view.
+     * @param tab The tab to show info about
      * @param fromShowSSLCertificateOnError The flag that indicates whether
      * this dialog was opened from the SSL-certificate-on-error dialog or
      * not. This is important, since we need to know whether to return to
      * the parent dialog or simply dismiss.
      */
-    private void showPageInfo(final WebView view,
+    private void showPageInfo(final TabControl.Tab tab,
                               final boolean fromShowSSLCertificateOnError) {
         final LayoutInflater factory = LayoutInflater
                 .from(this);
 
         final View pageInfoView = factory.inflate(R.layout.page_info, null);
+        
+        final WebView view = tab.getWebView();
 
         String url = null;
         String title = null;
 
-        // Use the cached title and url if this is the current WebView
-        if (view == mWebView) {
+        if (view == null) {
+            url = tab.getUrl();
+            title = tab.getTitle();
+        }else if (view == mWebView) {
+             // Use the cached title and url if this is the current WebView
             url = mUrl;
             title = mTitle;
         } else {
@@ -3603,7 +3677,7 @@
         ((TextView) pageInfoView.findViewById(R.id.address)).setText(url);
         ((TextView) pageInfoView.findViewById(R.id.title)).setText(title);
 
-        mPageInfoView = view;
+        mPageInfoView = tab;
         mPageInfoFromShowSSLCertificateOnError = new Boolean(fromShowSSLCertificateOnError);
 
         AlertDialog.Builder alertDialogBuilder =
@@ -3649,7 +3723,8 @@
 
         // if we have a main top-level page SSL certificate set or a certificate
         // error
-        if (fromShowSSLCertificateOnError || view.getCertificate() != null) {
+        if (fromShowSSLCertificateOnError || 
+                (view != null && view.getCertificate() != null)) {
             // add a 'View Certificate' button
             alertDialogBuilder.setNeutralButton(
                 R.string.view_certificate,
@@ -3671,7 +3746,7 @@
                             // otherwise, display the top-most certificate from
                             // the chain
                             if (view.getCertificate() != null) {
-                                showSSLCertificate(view);
+                                showSSLCertificate(tab);
                             }
                         }
                     }
@@ -3684,9 +3759,9 @@
        /**
      * Displays the main top-level page SSL certificate dialog
      * (accessible from the Page-Info dialog).
-     * @param view The target web-view.
+     * @param tab The tab to show certificate for.
      */
-    private void showSSLCertificate(final WebView view) {
+    private void showSSLCertificate(final TabControl.Tab tab) {
         final View certificateView =
             inflateCertificateView(mWebView.getCertificate());
         if (certificateView == null) {
@@ -3703,7 +3778,7 @@
         ((TextView)ll.findViewById(R.id.success))
             .setText(R.string.ssl_certificate_is_valid);
 
-        mSSLCertificateView = view;
+        mSSLCertificateView = tab;
         mSSLCertificateDialog =
             new AlertDialog.Builder(this)
                 .setTitle(R.string.ssl_certificate).setIcon(
@@ -3716,7 +3791,7 @@
                                 mSSLCertificateDialog = null;
                                 mSSLCertificateView = null;
 
-                                showPageInfo(view, false);
+                                showPageInfo(tab, false);
                             }
                         })
                 .setOnCancelListener(
@@ -3725,7 +3800,7 @@
                                 mSSLCertificateDialog = null;
                                 mSSLCertificateView = null;
 
-                                showPageInfo(view, false);
+                                showPageInfo(tab, false);
                             }
                         })
                 .show();
@@ -3811,7 +3886,8 @@
                                 // need to show the dialog again once the
                                 // user is done exploring the page-info details
 
-                                showPageInfo(view, true);
+                                showPageInfo(mTabControl.getTabFromView(view), 
+                                        true);
                             }
                         })
                 .setOnCancelListener(
@@ -4016,6 +4092,7 @@
                         .show();
             }
         }
+        mTabControl.getCurrentWebView().setNetworkAvailable(up);
     }
 
     @Override
@@ -4030,14 +4107,19 @@
                     if (extras != null && extras.getBoolean("new_window", false)) {
                         openTab(data);
                     } else {
+                        final TabControl.Tab currentTab =
+                                mTabControl.getCurrentTab();
                         // If the Window overview is up and we are not in the
                         // middle of an animation, animate away from it to the
                         // current tab.
                         if (mTabOverview != null && mAnimationCount == 0) {
-                            sendAnimateFromOverview(mTabControl.getCurrentTab(),
-                                    false, data, TAB_OVERVIEW_DELAY, null);
+                            sendAnimateFromOverview(currentTab, false, data,
+                                    TAB_OVERVIEW_DELAY, null);
                         } else {
-                            loadURL(data);
+                            dismissSubWindow(currentTab);
+                            if (data != null && data.length() != 0) {
+                                getTopWindow().loadUrl(data);
+                            }
                         }
                     }
                 }
@@ -4053,7 +4135,7 @@
      * menu to see the download window, or when a download changes state. It
      * shows the download window ontop of the current window.
      */
-    private void viewDownloads(Uri downloadRecord) {
+    /* package */ void viewDownloads(Uri downloadRecord) {
         Intent intent = new Intent(this,
                 BrowserDownloadPage.class);
         intent.setData(downloadRecord);
@@ -4071,30 +4153,41 @@
                 throw new AssertionError();
             }
 
-            mTabControl.removeTab(mTabControl.getTab(position));
+            // Remember the current tab.
+            TabControl.Tab current = mTabControl.getCurrentTab();
+            final TabControl.Tab remove = mTabControl.getTab(position);
+            mTabControl.removeTab(remove);
+            // If we removed the current tab, use the tab at position - 1 if
+            // possible.
+            if (current == remove) {
+                // If the user removes the last tab, act like the New Tab item
+                // was clicked on.
+                if (mTabControl.getTabCount() == 0) {
+                    current = mTabControl.createNewTab(false);
+                    sendAnimateFromOverview(current, true,
+                            mSettings.getHomePage(), TAB_OVERVIEW_DELAY, null);
+                } else {
+                    final int index = position > 0 ? (position - 1) : 0;
+                    current = mTabControl.getTab(index);
+                }
+            }
+
             // The tab overview could have been dismissed before this method is
             // called.
             if (mTabOverview != null) {
                 // Remove the tab and change the index.
-                mTabOverview.remove(position--);
-                mTabOverview.setCurrentIndex(position);
-            } else {
-                position--;
+                mTabOverview.remove(position);
+                mTabOverview.setCurrentIndex(mTabControl.getTabIndex(current));
             }
 
             // FIXME: This isn't really right. We don't have a current WebView
             // since we are switching between tabs and haven't selected a new
             // one. This just prevents a NPE in case the user hits home from the
             // tab switcher.
-            int index = position;
-            if (index == ImageGrid.NEW_TAB) {
-                index = 0;
-            }
-            final TabControl.Tab t = mTabControl.getTab(index);
             // Only the current tab ensures its WebView is non-null. This
             // implies that we are reloading the freed tab.
-            mTabControl.setCurrentTab(t);
-            mWebView = t.getWebView();
+            mTabControl.setCurrentTab(current);
+            mWebView = current.getWebView();
         }
         public void onClick(int index) {
             // Change the tab if necessary.
@@ -4112,13 +4205,10 @@
             // Clear all the data for tab picker so next time it will be
             // recreated.
             mTabControl.wipeAllPickerData();
-            BrowserActivity.this.getWindow().setFeatureInt(
-                    Window.FEATURE_PROGRESS, Window.PROGRESS_VISIBILITY_ON);
-            BrowserActivity.this.mMenuState = EMPTY_MENU;
 
             // NEW_TAB means that the "New Tab" cell was clicked on.
             if (index == ImageGrid.NEW_TAB) {
-                openTabAndShow(mSettings.getHomePage(), null);
+                openTabAndShow(mSettings.getHomePage(), null, false);
             } else {
                 sendAnimateFromOverview(mTabControl.getTab(index),
                         false, null, 0, null);
@@ -4156,11 +4246,13 @@
         protected void onDraw(Canvas canvas) {
             canvas.save();
             canvas.drawColor(Color.WHITE);
-            canvas.setDrawFilter(sZoomFilter);
-            float scale = getWidth() * mScale;
-            canvas.scale(scale, scale);
-            canvas.translate(-mScrollX, -mScrollY);
-            canvas.drawPicture(mPicture);
+            if (mPicture != null) {
+                canvas.setDrawFilter(sZoomFilter);
+                float scale = getWidth() * mScale;
+                canvas.scale(scale, scale);
+                canvas.translate(-mScrollX, -mScrollY);
+                canvas.drawPicture(mPicture);
+            }
             canvas.restore();
         }
     }
@@ -4219,12 +4311,13 @@
         // set it here to prevent another request to animate from coming in
         // between now and when ANIMATE_TO_OVERVIEW is handled.
         mAnimationCount++;
-        if (stay) {
-            getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null);
-            getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
-                    Window.PROGRESS_VISIBILITY_OFF);
-            setTitle(R.string.tab_picker_title);
-        }
+        // Always change the title bar to the window overview title while
+        // animating.
+        getWindow().setFeatureDrawable(Window.FEATURE_LEFT_ICON, null);
+        getWindow().setFeatureDrawable(Window.FEATURE_RIGHT_ICON, null);
+        getWindow().setFeatureInt(Window.FEATURE_PROGRESS,
+                Window.PROGRESS_VISIBILITY_OFF);
+        setTitle(R.string.tab_picker_title);
         // Make the menu empty until the animation completes.
         mMenuState = EMPTY_MENU;
     }
@@ -4254,14 +4347,13 @@
         startActivityForResult(intent, BOOKMARKS_PAGE);
     }
 
-    // Called when loading from bookmarks or goto.
-    private void loadURL(String url) {
+    // Called when loading from context menu or LOAD_URL message
+    private void loadURL(WebView view, String url) {
         // In case the user enters nothing.
-        if (url != null && url.length() != 0) {
+        if (url != null && url.length() != 0 && view != null) {
             url = smartUrlFilter(url);
-            WebView w = getTopWindow();
-            if (!mWebViewClient.shouldOverrideUrlLoading(w, url)) {
-                w.loadUrl(url);
+            if (!mWebViewClient.shouldOverrideUrlLoading(view, url)) {
+                view.loadUrl(url);
             }
         }
     }
@@ -4316,21 +4408,23 @@
      * @return Original or modified URL
      *
      */
-    String smartUrlFilter(String inUrl) {
+    String smartUrlFilter(String url) {
 
+        String inUrl = url.trim();
         boolean hasSpace = inUrl.indexOf(' ') != -1;
 
-        if (!hasSpace) {
-            Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
-            if (matcher.matches()) {
-                // force scheme to lowercase
-                String scheme = matcher.group(1);
-                String lcScheme = scheme.toLowerCase();
-                if (!lcScheme.equals(scheme)) {
-                    return lcScheme + matcher.group(2);
-                }
-                return inUrl;
+        Matcher matcher = ACCEPTED_URI_SCHEMA.matcher(inUrl);
+        if (matcher.matches()) {
+            if (hasSpace) {
+                inUrl = inUrl.replace(" ", "%20");
             }
+            // force scheme to lowercase
+            String scheme = matcher.group(1);
+            String lcScheme = scheme.toLowerCase();
+            if (!lcScheme.equals(scheme)) {
+                return lcScheme + matcher.group(2);
+            }
+            return inUrl;
         }
         if (hasSpace) {
             // FIXME: quick search, need to be customized by setting
@@ -4378,6 +4472,33 @@
                 QUERY_PLACE_HOLDER);
     }
 
+    /* package */void setBaseSearchUrl(String url) {
+        if (url == null || url.length() == 0) {
+            /*
+             * get the google search url based on the SIM. Default is US. NOTE:
+             * This code uses resources to optionally select the search Uri,
+             * based on the MCC value from the SIM. The default string will most
+             * likely be fine. It is parameterized to accept info from the
+             * Locale, the language code is the first parameter (%1$s) and the
+             * country code is the second (%2$s). This code must function in the
+             * same way as a similar lookup in
+             * com.android.googlesearch.SuggestionProvider#onCreate(). If you
+             * change either of these functions, change them both. (The same is
+             * true for the underlying resource strings, which are stored in
+             * mcc-specific xml files.)
+             */
+            Locale l = Locale.getDefault();
+            QuickSearch_G = getResources().getString(
+                    R.string.google_search_base, l.getLanguage(),
+                    l.getCountry().toLowerCase())
+                    + "client=ms-"
+                    + SystemProperties.get("ro.com.google.clientid", "unknown")
+                    + "&source=android-" + GOOGLE_SEARCH_SOURCE_SUGGEST + "&q=%s";
+        } else {
+            QuickSearch_G = url;
+        }
+    }
+
     private final static int LOCK_ICON_UNSECURE = 0;
     private final static int LOCK_ICON_SECURE   = 1;
     private final static int LOCK_ICON_MIXED    = 2;
@@ -4458,7 +4579,7 @@
     // As PageInfo has different style for landscape / portrait, we have
     // to re-open it when configuration changed
     private AlertDialog mPageInfoDialog;
-    private WebView mPageInfoView;
+    private TabControl.Tab mPageInfoView;
     // If the Page-Info dialog is launched from the SSL-certificate-on-error
     // dialog, we should not just dismiss it, but should get back to the
     // SSL-certificate-on-error dialog. This flag is used to store this state
@@ -4474,7 +4595,7 @@
     // as SSLCertificate has different style for landscape / portrait, we
     // have to re-open it when configuration changed
     private AlertDialog mSSLCertificateDialog;
-    private WebView mSSLCertificateView;
+    private TabControl.Tab mSSLCertificateView;
 
     // as HttpAuthentication has different style for landscape / portrait, we
     // have to re-open it when configuration changed
@@ -4487,10 +4608,7 @@
                                             ViewGroup.LayoutParams.FILL_PARENT);
     // We may provide UI to customize these
     // Google search from the browser
-    final static String QuickSearch_G =
-            "http://www.google.com/m?client=ms-"
-            + SystemProperties.get("ro.com.google.clientid", "unknown")
-            + "&source=android-chrome&q=%s";
+    static String QuickSearch_G;
     // Wikipedia search
     final static String QuickSearch_W = "http://en.wikipedia.org/w/index.php?search=%s&go=Go";
     // Dictionary search
@@ -4498,7 +4616,20 @@
     // Google Mobile Local search
     final static String QuickSearch_L = "http://www.google.com/m/search?site=local&q=%s&near=mountain+view";
 
-    private final static String QUERY_PLACE_HOLDER = "%s";
+    final static String QUERY_PLACE_HOLDER = "%s";
+
+    // "source" parameter for Google search through search key
+    final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key";
+    // "source" parameter for Google search through search menu
+    final static String GOOGLE_SEARCH_SOURCE_SEARCHMENU = "browser-menu";
+    // "source" parameter for Google search through goto menu
+    final static String GOOGLE_SEARCH_SOURCE_GOTO = "browser-goto";
+    // "source" parameter for Google search through simplily type
+    final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type";
+    // "source" parameter for Google search suggested by the browser
+    final static String GOOGLE_SEARCH_SOURCE_SUGGEST = "browser-suggest";
+    // "source" parameter for Google search from unknown source
+    final static String GOOGLE_SEARCH_SOURCE_UNKNOWN = "unknown";
 
     private final static String LOGTAG = "browser";
 
@@ -4516,6 +4647,13 @@
     // overlap. A count of 0 means no animation where a count of > 0 means
     // there are animations in progress.
     private int mAnimationCount;
+    
+    // As the ids are dynamically created, we can't guarantee that they will
+    // be in sequence, so this static array maps ids to a window number.
+    final static private int[] WINDOW_SHORTCUT_ID_ARRAY = 
+    { R.id.window_one_menu_id, R.id.window_two_menu_id, R.id.window_three_menu_id,
+      R.id.window_four_menu_id, R.id.window_five_menu_id, R.id.window_six_menu_id,
+      R.id.window_seven_menu_id, R.id.window_eight_menu_id };
 
     // monitor platform changes
     private IntentFilter mNetworkStateChangedFilter;
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
index 3b76e75..479dc0e 100644
--- a/src/com/android/browser/BrowserBookmarksAdapter.java
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -312,8 +312,21 @@
         if (url == null || favicon == null) {
             return;
         }
-        final String[] selArgs = new String[] { url };
-        final String where = Browser.BookmarkColumns.URL + " == ? AND "
+        // Strip the query.
+        int query = url.indexOf('?');
+        String noQuery = url;
+        if (query != -1) {
+            noQuery = url.substring(0, query);
+        }
+        url = noQuery + '?';
+        // Use noQuery to search for the base url (i.e. if the url is
+        // http://www.yahoo.com/?rs=1, search for http://www.yahoo.com)
+        // Use url to match the base url with other queries (i.e. if the url is
+        // http://www.google.com/m, search for
+        // http://www.google.com/m?some_query)
+        final String[] selArgs = new String[] { noQuery, url };
+        final String where = "(" + Browser.BookmarkColumns.URL + " == ? OR "
+                + Browser.BookmarkColumns.URL + " GLOB ? || '*') AND "
                 + Browser.BookmarkColumns.BOOKMARK + " == 1";
         final String[] projection = new String[] { Browser.BookmarkColumns._ID };
         final Cursor c = cr.query(Browser.BOOKMARKS_URI, projection, where,
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
index f938ff9..5c509a8 100644
--- a/src/com/android/browser/BrowserBookmarksPage.java
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -20,6 +20,7 @@
 import android.app.AlertDialog;
 import android.content.DialogInterface;
 import android.content.Intent;
+import android.graphics.Bitmap;
 import android.net.Uri;
 import android.os.Bundle;
 import android.os.Handler;
@@ -52,6 +53,10 @@
     private AddNewBookmark          mAddHeader;
     private boolean                 mCanceled = false;
     private boolean                 mCreateShortcut;
+    // XXX: There is no public string defining this intent so if Home changes
+    // the value, we have to update this string.
+    private static final String     INSTALL_SHORTCUT =
+            "com.android.launcher.action.INSTALL_SHORTCUT";
     
     private final static String LOGTAG = "browser";
 
@@ -80,6 +85,12 @@
         case R.id.edit_context_menu_id:
             editBookmark(i.position);
             break;
+        case R.id.shortcut_context_menu_id:
+            final Intent send = createShortcutIntent(getUrl(i.position),
+                    getBookmarkTitle(i.position));
+            send.setAction(INSTALL_SHORTCUT);
+            sendBroadcast(send);
+            break;
         case R.id.delete_context_menu_id:
             displayRemoveBookmarkDialog(i.position);
             break;
@@ -191,19 +202,27 @@
                     loadUrl(position);
                 }
             } else {
-                final Intent intent = new Intent();
-                intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(Intent.ACTION_VIEW,
-                        Uri.parse(getUrl(position))));
-                intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getBookmarkTitle(position));
-                intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
-                        Intent.ShortcutIconResource.fromContext(BrowserBookmarksPage.this,
-                                R.drawable.ic_launcher_browser));
+                final Intent intent = createShortcutIntent(getUrl(position),
+                        getBookmarkTitle(position));
                 setResult(RESULT_OK, intent);
                 finish();
             }
         }
     };
 
+    private Intent createShortcutIntent(String url, String title) {
+        final Intent i = new Intent();
+        i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, new Intent(Intent.ACTION_VIEW,
+                    Uri.parse(url)));
+        i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
+        i.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
+                Intent.ShortcutIconResource.fromContext(BrowserBookmarksPage.this,
+                        R.drawable.ic_launcher_browser));
+        // Do not allow duplicate items
+        i.putExtra("duplicate", false);
+        return i;
+    }
+
     private void saveCurrentPage() {
         Intent i = new Intent(BrowserBookmarksPage.this,
                 AddBookmarkPage.class);
@@ -290,7 +309,7 @@
         final int deletePos = position;
         new AlertDialog.Builder(this)
                 .setTitle(R.string.delete_bookmark)
-                .setIcon(R.drawable.ssl_icon)
+                .setIcon(android.R.drawable.ic_dialog_alert)
                 .setMessage(getText(R.string.delete_bookmark_warning).toString().replace(
                         "%s", getBookmarkTitle(deletePos)))
                 .setPositiveButton(R.string.ok, 
diff --git a/src/com/android/browser/BrowserDownloadAdapter.java b/src/com/android/browser/BrowserDownloadAdapter.java
index b3e08f5..0b509ef 100644
--- a/src/com/android/browser/BrowserDownloadAdapter.java
+++ b/src/com/android/browser/BrowserDownloadAdapter.java
@@ -20,7 +20,6 @@
 import android.content.ContentUris;
 import android.content.ContentValues;
 import android.content.Context;
-import android.content.Formatter;
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
@@ -30,6 +29,7 @@
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.provider.Downloads;
+import android.text.format.Formatter;
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
@@ -59,7 +59,7 @@
 
     public BrowserDownloadAdapter(Context context, int layout, Cursor c) {
         super(context, layout, c);
-        mFilenameColumnId = c.getColumnIndexOrThrow(Downloads.FILENAME);
+        mFilenameColumnId = c.getColumnIndexOrThrow(Downloads._DATA);
         mTitleColumnId = c.getColumnIndexOrThrow(Downloads.TITLE);
         mDescColumnId = c.getColumnIndexOrThrow(Downloads.DESCRIPTION);
         mStatusColumnId = c.getColumnIndexOrThrow(Downloads.STATUS);
@@ -213,7 +213,7 @@
                 return R.string.download_file_error;
                 
             case Downloads.STATUS_BAD_REQUEST:
-            case Downloads.STATUS_ERROR:
+            case Downloads.STATUS_UNKNOWN_ERROR:
             default:
                 return R.string.download_error;
         }
diff --git a/src/com/android/browser/BrowserDownloadPage.java b/src/com/android/browser/BrowserDownloadPage.java
index e2b11a6..4397337 100644
--- a/src/com/android/browser/BrowserDownloadPage.java
+++ b/src/com/android/browser/BrowserDownloadPage.java
@@ -75,7 +75,7 @@
         mDownloadCursor = managedQuery(Downloads.CONTENT_URI, 
                 new String [] {"_id", Downloads.TITLE, Downloads.STATUS,
                 Downloads.TOTAL_BYTES, Downloads.CURRENT_BYTES, 
-                Downloads.FILENAME, Downloads.DESCRIPTION, 
+                Downloads._DATA, Downloads.DESCRIPTION, 
                 Downloads.MIMETYPE, Downloads.LAST_MODIFICATION,
                 Downloads.VISIBILITY}, 
                 null, null);
@@ -170,6 +170,7 @@
                     (AdapterView.AdapterContextMenuInfo) menuInfo;
             mDownloadCursor.moveToPosition(info.position);
             mContextMenuPosition = info.position;
+            menu.setHeaderTitle(mDownloadCursor.getString(mTitleColumnId));
             
             MenuInflater inflater = getMenuInflater();
             int status = mDownloadCursor.getInt(mStatusColumnId);
@@ -242,10 +243,7 @@
      * @param id Row id of the download to resume
      */
     private void resumeDownload(final long id) {
-        Uri record = ContentUris.withAppendedId(Downloads.CONTENT_URI, id);
-        ContentValues values = new ContentValues();
-        values.put(Downloads.CONTROL, Downloads.CONTROL_RUN);
-        getContentResolver().update(record, values, null, null);
+        // the relevant functionality doesn't exist in the download manager
     }
     
     /**
@@ -327,7 +325,7 @@
      */
     private void cancelAllDownloads() {
         if (mDownloadCursor.moveToFirst()) {
-            StringBuffer where = new StringBuffer();
+            StringBuilder where = new StringBuilder();
             boolean firstTime = true;
             while (!mDownloadCursor.isAfterLast()) {
                 int status = mDownloadCursor.getInt(mStatusColumnId);
@@ -339,9 +337,9 @@
                     }
                     where.append("( ");
                     where.append(Downloads._ID);
-                    where.append(" = ");
+                    where.append(" = '");
                     where.append(mDownloadCursor.getLong(mIdColumnId));
-                    where.append(" )");
+                    where.append("' )");
                 }
                 mDownloadCursor.moveToNext();
             }
@@ -372,7 +370,7 @@
      */
     private void clearAllDownloads() {
         if (mDownloadCursor.moveToFirst()) {
-            StringBuffer where = new StringBuffer();
+            StringBuilder where = new StringBuilder();
             boolean firstTime = true;
             while (!mDownloadCursor.isAfterLast()) {
                 int status = mDownloadCursor.getInt(mStatusColumnId);
@@ -384,9 +382,9 @@
                     }
                     where.append("( ");
                     where.append(Downloads._ID);
-                    where.append(" = ");
+                    where.append(" = '");
                     where.append(mDownloadCursor.getLong(mIdColumnId));
-                    where.append(" )");
+                    where.append("' )");
                 }
                 mDownloadCursor.moveToNext();
             }
@@ -402,7 +400,7 @@
      */
     private void openCurrentDownload() {
         int filenameColumnId = 
-                mDownloadCursor.getColumnIndexOrThrow(Downloads.FILENAME);
+                mDownloadCursor.getColumnIndexOrThrow(Downloads._DATA);
         String filename = mDownloadCursor.getString(filenameColumnId);
         int mimetypeColumnId =
                 mDownloadCursor.getColumnIndexOrThrow(Downloads.MIMETYPE);
diff --git a/src/com/android/browser/BrowserHomepagePreference.java b/src/com/android/browser/BrowserHomepagePreference.java
new file mode 100644
index 0000000..bc21143
--- /dev/null
+++ b/src/com/android/browser/BrowserHomepagePreference.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.preference.EditTextPreference;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.util.Regex;
+import android.util.AttributeSet;
+
+public class BrowserHomepagePreference extends EditTextPreference implements
+        TextWatcher {
+
+    public BrowserHomepagePreference(Context context, AttributeSet attrs,
+            int defStyle) {
+        super(context, attrs, defStyle);
+        getEditText().addTextChangedListener(this);
+    }
+
+    public BrowserHomepagePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        getEditText().addTextChangedListener(this);
+    }
+
+    public BrowserHomepagePreference(Context context) {
+        super(context);
+        getEditText().addTextChangedListener(this);
+    }
+
+    public void afterTextChanged(Editable s) {
+        AlertDialog dialog = (AlertDialog) getDialog();
+        // This callback is called before the dialog has been fully constructed
+        if (dialog != null) {
+            dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(
+                    Regex.WEB_URL_PATTERN.matcher(s.toString()).matches());
+        }
+    }
+
+    public void beforeTextChanged(CharSequence s, int start, int count,
+            int after) {
+    }
+
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+    }
+}
diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java
index b8bc495..b23f750 100644
--- a/src/com/android/browser/BrowserPreferencesPage.java
+++ b/src/com/android/browser/BrowserPreferencesPage.java
@@ -51,6 +51,9 @@
                 getPreferenceScreen().getSharedPreferences()
                 .getString(BrowserSettings.PREF_TEXT_SIZE, null)) );
         
+        e = findPreference(BrowserSettings.PREF_DEFAULT_TEXT_ENCODING);
+        e.setOnPreferenceChangeListener(this);
+        
         if (BrowserSettings.getInstance().showDebugSettings()) {
             addPreferencesFromResource(R.xml.debug_preferences);
         }
@@ -76,30 +79,36 @@
             }
         } else if (pref.getKey().equals(BrowserSettings.PREF_HOMEPAGE)) {
             String value = (String) objValue;
-            
-            if (value.length() > 0) {
-                Uri path = Uri.parse(value);
-                if (path.getScheme() == null) {
-                    value = "http://"+value;
-                    
-                    pref.setSummary(value);
-                    
-                    // Update through the EditText control as it has a cached copy
-                    // of the string and it will handle persisting the value
-                    ((EditTextPreference)(pref)).setText(value);
-                    
-                    // as we update the value above, we need to return false
-                    // here so that setText() is not called by EditTextPref 
-                    // with the old value.
-                    return false;
-                }
+            boolean needUpdate = value.indexOf(' ') != -1;
+            if (needUpdate) {
+                value = value.trim().replace(" ", "%20");
             }
-            
+            Uri path = Uri.parse(value);
+            if (path.getScheme() == null) {
+                value = "http://" + value;
+                needUpdate = true;
+            }
+            // Set the summary value.
             pref.setSummary(value);
-            return true;
+            if (needUpdate) {
+                // Update through the EditText control as it has a cached copy
+                // of the string and it will handle persisting the value
+                ((EditTextPreference) pref).setText(value);
+
+                // as we update the value above, we need to return false
+                // here so that setText() is not called by EditTextPref
+                // with the old value.
+                return false;
+            } else {
+                return true;
+            }
         } else if (pref.getKey().equals(BrowserSettings.PREF_TEXT_SIZE)) {
             pref.setSummary(getVisualTextSizeName((String) objValue));
             return true;
+        } else if (pref.getKey().equals(
+                BrowserSettings.PREF_DEFAULT_TEXT_ENCODING)) {
+            pref.setSummary((String) objValue);
+            return true;
         }
         
         return false;
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index 4f456e7..7aa5bb2 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -30,6 +30,7 @@
 import android.database.sqlite.SQLiteOpenHelper;
 import android.database.sqlite.SQLiteDatabase;
 import android.net.Uri;
+import android.os.SystemProperties;
 import android.provider.Browser;
 import android.util.Log;
 import android.text.util.Regex;
@@ -101,6 +102,45 @@
 
     public BrowserProvider() {
     }
+  
+
+    private static CharSequence replaceSystemPropertyInString(CharSequence srcString) {
+        StringBuffer sb = new StringBuffer();
+        int lastCharLoc = 0;
+        for (int i = 0; i < srcString.length(); ++i) {
+            char c = srcString.charAt(i);
+            if (c == '{') {
+                sb.append(srcString.subSequence(lastCharLoc, i));
+                lastCharLoc = i;
+          inner:
+                for (int j = i; j < srcString.length(); ++j) {
+                    char k = srcString.charAt(j);
+                    if (k == '}') {
+                        String propertyKeyValue = srcString.subSequence(i + 1, j).toString();
+                        // See if the propertyKeyValue specifies a default value
+                        int defaultOffset = propertyKeyValue.indexOf(':');
+                        if (defaultOffset == -1) {
+                            sb.append(SystemProperties.get(propertyKeyValue));
+                        } else {
+                            String propertyKey = propertyKeyValue.substring(0, defaultOffset);
+                            String defaultValue = 
+                                    propertyKeyValue.substring(defaultOffset + 1, 
+                                                               propertyKeyValue.length());
+                            sb.append(SystemProperties.get(propertyKey, defaultValue));
+                        }
+                        lastCharLoc = j + 1;
+                        i = j;
+                        break inner;
+                    }
+                }
+            }
+        }
+        if (srcString.length() - lastCharLoc > 0) {
+            // Put on the tail, if there is one
+            sb.append(srcString.subSequence(lastCharLoc, srcString.length()));
+        }
+        return sb;
+    }
 
     private static class DatabaseHelper extends SQLiteOpenHelper {
         private Context mContext;
@@ -129,9 +169,10 @@
             int size = bookmarks.length;
             try {
                 for (int i = 0; i < size; i = i + 2) {
+                    CharSequence bookmarkDestination = replaceSystemPropertyInString(bookmarks[i + 1]);
                     db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
                             "date, created, bookmark)" + " VALUES('" +
-                            bookmarks[i] + "', '" + bookmarks[i + 1] + 
+                            bookmarks[i] + "', '" + bookmarkDestination + 
                             "', 0, 0, 0, 1);");
                 }
             } catch (ArrayIndexOutOfBoundsException e) {
@@ -401,12 +442,18 @@
                 myArgs = null;
             } else {
                 String like = selectionArgs[0] + "%";
-                SUGGEST_ARGS[0] = "http://" + like;
-                SUGGEST_ARGS[1] = "http://www." + like;
-                SUGGEST_ARGS[2] = "https://" + like;
-                SUGGEST_ARGS[3] = "https://www." + like;
-                myArgs = SUGGEST_ARGS;
-                suggestSelection = SUGGEST_SELECTION;
+                if (selectionArgs[0].startsWith("http")) {
+                    myArgs = new String[1];
+                    myArgs[0] = like;
+                    suggestSelection = selection;
+                } else {
+                    SUGGEST_ARGS[0] = "http://" + like;
+                    SUGGEST_ARGS[1] = "http://www." + like;
+                    SUGGEST_ARGS[2] = "https://" + like;
+                    SUGGEST_ARGS[3] = "https://www." + like;
+                    myArgs = SUGGEST_ARGS;
+                    suggestSelection = SUGGEST_SELECTION;
+                }
             }
             // Suggestions are always performed with the default sort order:
             // date ASC.
diff --git a/src/com/android/browser/BrowserSearchpagePreference.java b/src/com/android/browser/BrowserSearchpagePreference.java
new file mode 100644
index 0000000..09e8993
--- /dev/null
+++ b/src/com/android/browser/BrowserSearchpagePreference.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.preference.EditTextPreference;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.text.util.Regex;
+import android.util.AttributeSet;
+
+public class BrowserSearchpagePreference extends EditTextPreference implements
+        TextWatcher {
+
+    public BrowserSearchpagePreference(Context context, AttributeSet attrs,
+            int defStyle) {
+        super(context, attrs, defStyle);
+        getEditText().addTextChangedListener(this);
+    }
+
+    public BrowserSearchpagePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        getEditText().addTextChangedListener(this);
+    }
+
+    public BrowserSearchpagePreference(Context context) {
+        super(context);
+        getEditText().addTextChangedListener(this);
+    }
+
+    public void afterTextChanged(Editable s) {
+        AlertDialog dialog = (AlertDialog) getDialog();
+        // This callback is called before the dialog has been fully constructed
+        if (dialog != null) {
+            String string = s.toString();
+            int length = string.length();
+            int first = length > 0 ? string
+                    .indexOf(BrowserActivity.QUERY_PLACE_HOLDER) : -1;
+            int last = length > 0 ? string
+                    .lastIndexOf(BrowserActivity.QUERY_PLACE_HOLDER) : -1;
+            dialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(
+                    length == 0 || (first > 0 && first == last));
+        }
+    }
+
+    public void beforeTextChanged(CharSequence s, int start, int count,
+            int after) {
+    }
+
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+    }
+}
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index b19c02e..6164e38 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -22,6 +22,7 @@
 import android.content.pm.ActivityInfo;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
+import android.os.SystemProperties;
 import android.view.WindowManager;
 import android.webkit.CacheManager;
 import android.webkit.CookieManager;
@@ -63,7 +64,8 @@
     private boolean saveFormData = true;
     private boolean openInBackground = false;
     private String defaultTextEncodingName;
-    private String homeUrl = "http://www.google.com/m";
+    private String homeUrl = "http://www.google.com/m?client=ms-" + 
+        SystemProperties.get("ro.com.google.clientid", "unknown");
     private boolean loginInitialized = false;
     private int orientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
     private boolean autoFitPage = true;
@@ -102,7 +104,17 @@
     public final static String PREF_DEBUG_SETTINGS = "debug_menu";
     public final static String PREF_GEARS_SETTINGS = "gears_settings";
     public final static String PREF_TEXT_SIZE = "text_size";
-    
+    public final static String PREF_DEFAULT_TEXT_ENCODING =
+            "default_text_encoding";
+
+    private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (Macintosh; " +
+            "U; Intel Mac OS X 10_5_5; en-us) AppleWebKit/525.18 (KHTML, " +
+            "like Gecko) Version/3.1.2 Safari/525.20.1";
+
+    private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " +
+            "CPU iPhone OS 2_2 like Mac OS X; en-us) AppleWebKit/525.18.1 " +
+            "(KHTML, like Gecko) Version/3.1.1 Mobile/5G77 Safari/525.20";
+
     // Value to truncate strings when adding them to a TextView within
     // a ListView
     public final static int MAX_TEXTVIEW_LEN = 80;
@@ -134,7 +146,14 @@
             WebSettings s = mSettings;
 
             s.setLayoutAlgorithm(b.layoutAlgorithm);
-            s.setUserAgent(b.userAgent);
+            if (b.userAgent == 0) {
+                // use the default ua string
+                s.setUserAgentString(null);
+            } else if (b.userAgent == 1) {
+                s.setUserAgentString(DESKTOP_USERAGENT);
+            } else if (b.userAgent == 2) {
+                s.setUserAgentString(IPHONE_USERAGENT);
+            }
             s.setUseWideViewPort(b.useWideViewPort);
             s.setLoadsImagesAutomatically(b.loadsImagesAutomatically);
             s.setJavaScriptEnabled(b.javaScriptEnabled);
@@ -157,6 +176,8 @@
             s.setNeedInitialFocus(false);
             // Browser supports multiple windows
             s.setSupportMultipleWindows(true);
+            // Turn off file access
+            s.setAllowFileAccess(false);
         }
     }
    
@@ -218,6 +239,9 @@
         } else {
             layoutAlgorithm = WebSettings.LayoutAlgorithm.NORMAL;
         }
+        defaultTextEncodingName =
+                p.getString(PREF_DEFAULT_TEXT_ENCODING,
+                        defaultTextEncodingName);
         
         showDebugSettings = 
                 p.getBoolean(PREF_DEBUG_SETTINGS, showDebugSettings);
@@ -244,6 +268,8 @@
             navDump = p.getBoolean("enable_nav_dump", navDump);
             doFlick = p.getBoolean("enable_flick", doFlick);
             userAgent = Integer.parseInt(p.getString("user_agent", "0"));
+            mTabControl.getBrowserActivity().setBaseSearchUrl(
+                    p.getString("search_url", ""));
         }
         update();
     }
@@ -398,10 +424,6 @@
         ContentResolver resolver = context.getContentResolver();
         Browser.clearHistory(resolver);
         Browser.clearSearches(resolver);
-        // Delete back-forward list
-        if (mTabControl != null) {
-            mTabControl.clearHistory();
-        }
     }
 
     /* package */ void clearFormData(Context context) {
diff --git a/src/com/android/browser/FakeWebView.java b/src/com/android/browser/FakeWebView.java
index 200f86a..7997672 100644
--- a/src/com/android/browser/FakeWebView.java
+++ b/src/com/android/browser/FakeWebView.java
@@ -70,12 +70,14 @@
                 final WebView w = mTab.getTopWindow();
                 if (w != null) {
                     Picture p = w.capturePicture();
-                    canvas.save();
-                    float scale = getWidth() * w.getScale() / w.getWidth();
-                    canvas.scale(scale, scale);
-                    canvas.translate(-w.getScrollX(), -w.getScrollY());
-                    canvas.drawPicture(p);
-                    canvas.restore();
+                    if (p != null) {
+                        canvas.save();
+                        float scale = getWidth() * w.getScale() / w.getWidth();
+                        canvas.scale(scale, scale);
+                        canvas.translate(-w.getScrollX(), -w.getScrollY());
+                        canvas.drawPicture(p);
+                        canvas.restore();
+                    }
                 }
             }
         }
diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java
new file mode 100644
index 0000000..8578643
--- /dev/null
+++ b/src/com/android/browser/FetchUrlMimeType.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.content.ContentValues;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.Header;
+import org.apache.http.client.methods.HttpHead;
+
+import java.io.IOException;
+
+import android.os.AsyncTask;
+import android.provider.Downloads;
+import android.webkit.MimeTypeMap;
+import android.webkit.URLUtil;
+
+/**
+ * This class is used to pull down the http headers of a given URL so that
+ * we can analyse the mimetype and make any correction needed before we give
+ * the URL to the download manager. The ContentValues class holds the
+ * content that would be provided to the download manager, so that on
+ * completion of checking the mimetype, we can issue the download to
+ * the download manager.
+ * This operation is needed when the user long-clicks on a link or image and
+ * we don't know the mimetype. If the user just clicks on the link, we will
+ * do the same steps of correcting the mimetype down in
+ * android.os.webkit.LoadListener rather than handling it here.
+ *
+ */
+class FetchUrlMimeType extends AsyncTask<ContentValues, String, String> {
+
+    BrowserActivity mActivity;
+    ContentValues mValues;
+
+    public FetchUrlMimeType(BrowserActivity activity) {
+        mActivity = activity;
+    }
+
+    @Override
+    public String doInBackground(ContentValues... values) {
+        mValues = values[0];
+
+        // Check to make sure we have a URI to download
+        String uri = mValues.getAsString(Downloads.URI);
+        if (uri == null || uri.length() == 0) {
+            return null;
+        }
+
+        // User agent is likely to be null, though the AndroidHttpClient
+        // seems ok with that.
+        AndroidHttpClient client = AndroidHttpClient.newInstance(
+                mValues.getAsString(Downloads.USER_AGENT));
+        HttpHead request = new HttpHead(uri);
+
+        String cookie = mValues.getAsString(Downloads.COOKIE_DATA);
+        if (cookie != null && cookie.length() > 0) {
+            request.addHeader("Cookie", cookie);
+        }
+
+        String referer = mValues.getAsString(Downloads.REFERER);
+        if (referer != null && referer.length() > 0) {
+            request.addHeader("Referer", referer);
+        }
+
+        HttpResponse response;
+        Boolean succeeded = true;
+        String mimeType = null;
+        try {
+            response = client.execute(request);
+            // We could get a redirect here, but if we do lets let
+            // the download manager take care of it, and thus trust that
+            // the server sends the right mimetype
+            if (response.getStatusLine().getStatusCode() == 200) {
+                Header header = response.getFirstHeader("Content-Type");
+                if (header != null) {
+                    mimeType = header.getValue();
+                    final int semicolonIndex = mimeType.indexOf(';');
+                    if (semicolonIndex != -1) {
+                        mimeType = mimeType.substring(0, semicolonIndex);
+                    }
+                }
+            }
+        } catch (IllegalArgumentException ex) {
+            request.abort();
+        } catch (IOException ex) {
+            request.abort();
+        } finally {
+            client.close();
+        }
+
+        return mimeType;
+    }
+
+   @Override
+    public void onPostExecute(String mimeType) {
+       if (mimeType != null) {
+           String url = mValues.getAsString(Downloads.URI);
+           if (mimeType.equalsIgnoreCase("text/plain") ||
+                   mimeType.equalsIgnoreCase("application/octet-stream")) {
+               String newMimeType =
+                       MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+                           MimeTypeMap.getFileExtensionFromUrl(url));
+               if (newMimeType != null) {
+                   mValues.put(Downloads.MIMETYPE, newMimeType);
+               }
+           }
+           String filename = URLUtil.guessFileName(url,
+                   null, mimeType);
+           mValues.put(Downloads.FILENAME_HINT, filename);
+       }
+
+       // Start the download
+       final Uri contentUri =
+           mActivity.getContentResolver().insert(Downloads.CONTENT_URI, mValues);
+       mActivity.viewDownloads(contentUri);
+    }
+
+}
diff --git a/src/com/android/browser/FindDialog.java b/src/com/android/browser/FindDialog.java
index 42447e3..2b26784 100644
--- a/src/com/android/browser/FindDialog.java
+++ b/src/com/android/browser/FindDialog.java
@@ -16,22 +16,24 @@
 
 package com.android.browser;
 
+import android.app.Dialog;
+import android.content.res.Configuration;
+import android.os.Bundle;
 import android.os.Handler;
 import android.os.Message;
 import android.text.Editable;
 import android.text.Spannable;
 import android.text.TextWatcher;
+import android.view.Gravity;
 import android.view.KeyEvent;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.LayoutInflater;
+import android.view.Window;
 import android.webkit.WebView;
 import android.widget.EditText;
-import android.widget.LinearLayout;
 import android.widget.TextView;
 
-/* package */ class FindDialog extends LinearLayout implements TextWatcher {
+/* package */ class FindDialog extends Dialog implements TextWatcher {
     private WebView         mWebView;
     private TextView        mMatches;
     private BrowserActivity mBrowserActivity;
@@ -41,10 +43,7 @@
     private EditText        mEditText;
     private View            mNextButton;
     private View            mPrevButton;
-
-    // Tags for messages to be sent to the handler.
-    private final static int FIND_RESPONSE  = 0;
-    private final static int NUM_FOUND      = 1;
+    private View            mMatchesView;
 
     private View.OnClickListener mFindListener = new View.OnClickListener() {
         public void onClick(View v) {
@@ -65,26 +64,7 @@
             if (mWebView == null) {
                 throw new AssertionError("No WebView for FindDialog::onClick");
             }
-            // Find is disabled for version 1.0, so find methods on WebView are
-            // currently private.
-            //mWebView.findPrevious(mEditText.getText().toString(),
-            //        mFindHandler.obtainMessage(FIND_RESPONSE));
-        }
-    };
-    
-    private Handler mFindHandler = new Handler() {
-        public void handleMessage(Message msg) {
-            if (NUM_FOUND == msg.what) {
-                mMatches.setText(Integer.toString(msg.arg1));
-                if (0 == msg.arg1) {
-                    disableButtons();
-                } else {
-                    mPrevButton.setFocusable(true);
-                    mNextButton.setFocusable(true);
-                    mPrevButton.setEnabled(true);
-                    mNextButton.setEnabled(true);
-                }
-            }
+            mWebView.findNext(false);
         }
     };
     
@@ -95,20 +75,35 @@
         mNextButton.setFocusable(false);
     }
 
-    public void setWebView(WebView webview) {
+    /* package */ void setWebView(WebView webview) {
         mWebView = webview;
     }
 
     /* package */ FindDialog(BrowserActivity context) {
-        super(context);
+        super(context, R.style.FindDialogTheme);
         mBrowserActivity = context;
-        LayoutInflater factory = LayoutInflater.from(context);
-        factory.inflate(R.layout.browser_find, this);
-        
-        setLayoutParams(new ViewGroup.LayoutParams(
-                ViewGroup.LayoutParams.FILL_PARENT,
-                ViewGroup.LayoutParams.WRAP_CONTENT));
-        
+        setCanceledOnTouchOutside(true);
+    }
+
+    /* package */ void onConfigurationChanged(Configuration newConfig) {
+        // FIXME: Would like to call mWebView.findAll again, so that the
+        // matches would refresh, but the new picture has not yet been
+        // created, so it is too soon.
+        mEditText.getText().clear();
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        Window theWindow = getWindow();
+        theWindow.setGravity(Gravity.BOTTOM|Gravity.FILL_HORIZONTAL);
+
+        setContentView(R.layout.browser_find);
+
+        theWindow.setLayout(ViewGroup.LayoutParams.FILL_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+
         mEditText = (EditText) findViewById(R.id.edit);
         
         View button = findViewById(R.id.next);
@@ -124,47 +119,21 @@
         mOk = button;
         
         mMatches = (TextView) findViewById(R.id.matches);
+        mMatchesView = findViewById(R.id.matches_view);
         disableButtons();
     }
     
     public void dismiss() {
+        super.dismiss();
         mBrowserActivity.closeFind();
-        // If the nav buttons are highlighted, then there are matches
-        // highlighted in the WebView, and they should be cleared.
-        if (mPrevButton.isEnabled()) {
-            // Find is disabled for version 1.0, so find methods on WebView are
-            // currently private.
-            //mWebView.clearMatches();
-        }
+        mWebView.clearMatches();
     }
     
     @Override
     public boolean dispatchKeyEvent(KeyEvent event) {
-        // Make up and down find previous/next
         int code = event.getKeyCode();
         boolean up = event.getAction() == KeyEvent.ACTION_UP;
         switch (code) {
-            case KeyEvent.KEYCODE_BACK:
-                if (up) {
-                    dismiss();
-                }
-                return true;
-            case KeyEvent.KEYCODE_DPAD_UP:
-                if (event.getMetaState() != 0) {
-                    break;
-                }
-                if (up) {
-                    mFindPreviousListener.onClick(null);
-                }
-                return true;
-            case KeyEvent.KEYCODE_DPAD_DOWN:
-                if (event.getMetaState() != 0) {
-                    break;
-                }
-                if (up) {
-                    mFindListener.onClick(null);
-                }
-                return true;
             case KeyEvent.KEYCODE_DPAD_CENTER:
             case KeyEvent.KEYCODE_ENTER:
                 if (!mEditText.hasFocus()) {
@@ -179,26 +148,16 @@
         }
         return super.dispatchKeyEvent(event);
     }
-    
-    @Override
-    public boolean dispatchTouchEvent(MotionEvent ev) {
-        super.dispatchTouchEvent(ev);
-        // Return true so that BrowserActivity thinks we handled it and does
-        // not dismiss us.
-        return true;
-    }
 
     private void findNext() {
         if (mWebView == null) {
             throw new AssertionError("No WebView for FindDialog::findNext");
         }
-        // Find is disabled for version 1.0, so find methods on WebView are
-        // currently private.
-        //mWebView.findNext(mEditText.getText().toString(),
-        //        mFindHandler.obtainMessage(FIND_RESPONSE));
+        mWebView.findNext(true);
     }
     
     public void show() {
+        super.show();
         mEditText.requestFocus();
         mEditText.setText("");
         Spannable span = (Spannable) mEditText.getText();
@@ -219,22 +178,30 @@
                               int start, 
                               int before, 
                               int count) {
+        if (mWebView == null) {
+            throw new AssertionError(
+                    "No WebView for FindDialog::onTextChanged");
+        }
         CharSequence find = mEditText.getText();
         if (0 == find.length()) {
             disableButtons();
-            // Find is disabled for version 1.0, so find methods on WebView are
-            // currently private.
-            //mWebView.clearMatches();
-            mMatches.setText(R.string.zero);
+            mWebView.clearMatches();
+            mMatchesView.setVisibility(View.INVISIBLE);
         } else {
-            if (mWebView == null) {
-                throw new AssertionError(
-                        "No WebView for FindDialog::onTextChanged");
+            mMatchesView.setVisibility(View.VISIBLE);
+            int found = mWebView.findAll(find.toString());
+            mMatches.setText(Integer.toString(found));
+            if (found < 2) {
+                disableButtons();
+                if (found == 0) {
+                    mMatches.setText(R.string.zero);
+                }
+            } else {
+                mPrevButton.setFocusable(true);
+                mNextButton.setFocusable(true);
+                mPrevButton.setEnabled(true);
+                mNextButton.setEnabled(true);
             }
-            // Find is disabled for version 1.0, so find methods on WebView are
-            // currently private.
-            //mWebView.findAll(find.toString(),
-            //        mFindHandler.obtainMessage(NUM_FOUND));
         }
     }
 
diff --git a/src/com/android/browser/GearsBaseDialog.java b/src/com/android/browser/GearsBaseDialog.java
new file mode 100644
index 0000000..c930dc8
--- /dev/null
+++ b/src/com/android/browser/GearsBaseDialog.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.Handler;
+import android.util.Log;
+import android.view.InflateException;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.lang.ClassCastException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Base dialog class for gears
+ */
+class GearsBaseDialog {
+
+  private static final String TAG = "GearsNativeDialog";
+  protected Handler mHandler;
+  protected Activity mActivity;
+  protected String mDialogArguments;
+
+  private Bitmap mIcon;
+  private final int MAX_ICON_SIZE = 64;
+  protected int mChoosenIconSize;
+
+  // Dialog closing types
+  public static final int CANCEL = 0;
+  public static final int ALWAYS_DENY = 1;
+  public static final int ALLOW = 2;
+  public static final int DENY = 3;
+  public static final int NEW_ICON = 4;
+  public static final int UPDATE_ICON = 5;
+  public static final int REQUEST_ICON = 6;
+  public static final int PAUSE_REQUEST_ICON = 7;
+
+  protected final String LOCAL_DATA_STRING = "localData";
+  protected final String LOCAL_STORAGE_STRING = "localStorage";
+  protected final String LOCATION_DATA_STRING = "locationData";
+
+  protected String mGearsVersion = "UNDEFINED";
+  protected boolean mDebug = false;
+
+  public GearsBaseDialog(Activity activity, Handler handler, String arguments) {
+    mActivity = activity;
+    mHandler = handler;
+    mDialogArguments = arguments;
+  }
+
+  Resources getResources() {
+    return mActivity.getResources();
+  }
+
+  Object getSystemService(String name) {
+    return mActivity.getSystemService(name);
+  }
+
+  View findViewById(int id) {
+    return mActivity.findViewById(id);
+  }
+
+  private String getString(int id) {
+    return mActivity.getString(id);
+  }
+
+  public void setDebug(boolean debug) {
+    mDebug = debug;
+  }
+
+  public void setGearsVersion(String version) {
+    mGearsVersion = version;
+  }
+
+  public String closeDialog(int closingType) {
+    return null;
+  }
+
+  /*
+   * Utility methods for setting up the dialogs elements
+   */
+
+  /**
+   * Inflate a given layout in a view (which has to be
+   * a ViewGroup, e.g. LinearLayout).
+   * This is used to share the basic dialog outline among
+   * the different dialog types.
+   */
+  void inflate(int layout, int viewID) {
+    LayoutInflater inflater = (LayoutInflater) getSystemService(
+        Context.LAYOUT_INFLATER_SERVICE);
+    View view = findViewById(viewID);
+    if (view != null) {
+      try {
+        ViewGroup viewGroup = (ViewGroup) view;
+        inflater.inflate(layout, viewGroup);
+      } catch (ClassCastException e) {
+        String msg = "exception, the view (" + view + ")";
+        msg += " is not a ViewGroup";
+        Log.e(TAG, msg, e);
+      } catch (InflateException e) {
+        Log.e(TAG, "exception while inflating the layout", e);
+      }
+    } else {
+      String msg = "problem, trying to inflate a non-existent view";
+      msg += " (" + viewID + ")";
+      Log.e(TAG, msg);
+    }
+  }
+
+  /**
+   * Button setup.
+   * Set the button's text and its listener. If the text resource's id
+   * is 0, makes the button invisible.
+   */
+  void setupButton(int buttonRscID,
+                   int rscString,
+                   View.OnClickListener listener) {
+    View view = findViewById(buttonRscID);
+    if (view == null) {
+      return;
+    }
+    Button button = (Button) view;
+
+    if (rscString == 0) {
+      button.setVisibility(View.GONE);
+    } else {
+      CharSequence text = getString(rscString);
+      button.setText(text);
+      button.setOnClickListener(listener);
+    }
+  }
+
+  /**
+   * Utility method to setup the three dialog buttons.
+   */
+  void setupButtons(int alwaysDenyRsc, int allowRsc, int denyRsc) {
+    setupButton(R.id.button_alwaysdeny, alwaysDenyRsc,
+                new Button.OnClickListener() {
+                  public void onClick(View v) {
+                    mHandler.sendEmptyMessage(ALWAYS_DENY);
+                  }
+                });
+
+    setupButton(R.id.button_allow, allowRsc,
+                new Button.OnClickListener() {
+                  public void onClick(View v) {
+                    mHandler.sendEmptyMessage(ALLOW);
+                  }
+                });
+
+    setupButton(R.id.button_deny, denyRsc,
+                new Button.OnClickListener() {
+                  public void onClick(View v) {
+                    mHandler.sendEmptyMessage(DENY);
+                  }
+                });
+  }
+
+  /**
+   * Utility method to set elements' text indicated in
+   * the dialogs' arguments.
+   */
+  void setLabel(JSONObject json, String name, int rsc) {
+    try {
+      if (json.has(name)) {
+        String text = json.getString(name);
+        View view = findViewById(rsc);
+        if (view != null && text != null) {
+          TextView textView = (TextView) view;
+          textView.setText(text);
+          textView.setVisibility(View.VISIBLE);
+        }
+      }
+    } catch (JSONException e) {
+      Log.e(TAG, "json exception", e);
+    }
+  }
+
+  /**
+   * Utility class to download an icon in the background.
+   * Once done ask the UI thread to update the icon.
+   */
+  class IconDownload implements Runnable {
+    private String mUrlString;
+
+    IconDownload(String url) {
+      mUrlString = url;
+    }
+
+    public void run() {
+      if (mUrlString == null) {
+        return;
+      }
+      try {
+        URL url = new URL(mUrlString);
+        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+        connection.setDoInput(true);
+        connection.connect();
+        int length = connection.getContentLength();
+        InputStream is = connection.getInputStream();
+        Bitmap customIcon = BitmapFactory.decodeStream(is);
+        if (customIcon != null) {
+          mIcon = customIcon;
+          mHandler.sendEmptyMessage(UPDATE_ICON);
+        }
+      } catch (ClassCastException e) {
+        Log.e(TAG, "Class cast exception (" + mUrlString + ")", e);
+      } catch (MalformedURLException e) {
+        Log.e(TAG, "Malformed url (" + mUrlString + ") ", e);
+      } catch (IOException e) {
+        Log.e(TAG, "Exception downloading icon (" + mUrlString + ") ", e);
+      }
+    }
+  }
+
+  /**
+   * Utility method to update the icon.
+   * Called on the UI thread.
+   */
+  public void updateIcon() {
+    if (mIcon == null) {
+      return;
+    }
+    View view = findViewById(R.id.origin_icon);
+    if (view != null) {
+      ImageView imageView = (ImageView) view;
+      imageView.setMaxHeight(MAX_ICON_SIZE);
+      imageView.setMaxWidth(MAX_ICON_SIZE);
+      imageView.setScaleType(ImageView.ScaleType.FIT_XY);
+      imageView.setImageBitmap(mIcon);
+      imageView.setVisibility(View.VISIBLE);
+    }
+  }
+
+  /**
+   * Utility method to download an icon from a url and set
+   * it to the GUI element R.id.origin_icon.
+   * It is used both in the shortcut dialog and the
+   * permission dialog.
+   * The actual download is done in the background via
+   * IconDownload; once the icon is downlowded the UI is updated
+   * via updateIcon().
+   * The icon size is included in the layout with the choosen
+   * size, although not displayed, to limit text reflow once
+   * the icon is received.
+   */
+  void downloadIcon(String url) {
+    if (url == null) {
+      return;
+    }
+    View view = findViewById(R.id.origin_icon);
+    if (view != null) {
+      view.setMinimumWidth(mChoosenIconSize);
+      view.setMinimumHeight(mChoosenIconSize);
+      view.setVisibility(View.INVISIBLE);
+    }
+    Thread thread = new Thread(new IconDownload(url));
+    thread.start();
+  }
+
+  /**
+   * Utility method that get the dialogMessage
+   * and icon and ask the setupDialog(message,icon)
+   * method to set the values.
+   */
+  public void setupDialog() {
+    TextView dialogMessage = null;
+    ImageView icon = null;
+
+    View view = findViewById(R.id.dialog_message);
+    if (view != null) {
+      dialogMessage = (TextView) view;
+    }
+
+    View iconView = findViewById(R.id.icon);
+    if (iconView != null) {
+      icon = (ImageView) iconView;
+    }
+
+    if ((dialogMessage != null) && (icon != null)) {
+      setupDialog(dialogMessage, icon);
+      dialogMessage.setVisibility(View.VISIBLE);
+    }
+  }
+
+  /*
+   * Set the message and icon of the dialog
+   */
+  public void setupDialog(TextView message, ImageView icon) {
+    message.setText(R.string.unrecognized_dialog_message);
+    icon.setImageResource(R.drawable.gears_icon_48x48);
+    message.setVisibility(View.VISIBLE);
+  }
+
+  /**
+   * Setup the dialog
+   * By default, just display a simple message.
+   */
+  public void setup() {
+    setupButtons(0, 0, R.string.default_button);
+    setupDialog();
+  }
+
+
+}
diff --git a/src/com/android/browser/GearsDialog.java b/src/com/android/browser/GearsDialog.java
index fd9e762..62a8aaa 100644
--- a/src/com/android/browser/GearsDialog.java
+++ b/src/com/android/browser/GearsDialog.java
@@ -48,6 +48,8 @@
   private String htmlContent;
   private String dialogArguments;
 
+  private boolean dismissed = false;
+
   @Override
   public void onCreate(Bundle icicle) {
     super.onCreate(icicle);
@@ -66,14 +68,24 @@
   }
 
   @Override
+  public void onDestroy() {
+    super.onDestroy();
+    // In case we reach this point without
+    // notifying GearsDialogService, we do it now.
+    if (!dismissed) {
+      notifyEndOfDialog();
+    }
+  }
+
+  @Override
   public void onConfigurationChanged(Configuration newConfig) {
     super.onConfigurationChanged(newConfig);
     Intent i = getIntent();
     boolean inSettings = i.getBooleanExtra("inSettings", false);
     // If we are called from the settings, we
-    // dismiss ourselve upon upon rotation
+    // dismiss ourselve upon rotation
     if (inSettings) {
-      GearsDialogService.signalFinishedDialog();
+      notifyEndOfDialog();
       finish();
     }
   }
@@ -88,10 +100,22 @@
     webview.loadDataWithBaseURL("", htmlContent, "text/html", "", "");
   }
 
+  /**
+   * Signal to GearsDialogService that we are done.
+   */
+  private void notifyEndOfDialog() {
+    GearsDialogService.signalFinishedDialog();
+    dismissed = true;
+  }
+
+  /**
+   * Intercepts the back key to immediately notify
+   * GearsDialogService that we are done.
+   */
   public boolean dispatchKeyEvent(KeyEvent event) {
     if (event.getKeyCode() ==  KeyEvent.KEYCODE_BACK && event.isDown()) {
-      GearsDialogService.signalFinishedDialog();
-     }
+      notifyEndOfDialog();
+    }
     return super.dispatchKeyEvent(event);
   }
 
@@ -110,7 +134,7 @@
    */
   public void closeDialog(String results) {
     GearsDialogService.closeDialog(results);
-    GearsDialogService.signalFinishedDialog();
+    notifyEndOfDialog();
     finish();
   }
 
diff --git a/src/com/android/browser/GearsFilePickerDialog.java b/src/com/android/browser/GearsFilePickerDialog.java
new file mode 100644
index 0000000..4a3c0ed
--- /dev/null
+++ b/src/com/android/browser/GearsFilePickerDialog.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.net.Uri;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.MediaStore;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Vector;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+
+/**
+ * Gears FilePicker dialog
+ */
+class GearsFilePickerDialog extends GearsBaseDialog
+  implements View.OnTouchListener {
+
+  private static final String TAG = "Gears FilePicker";
+  private static Bitmap mDirectoryIcon;
+  private static Bitmap mDefaultIcon;
+  private static Bitmap mImageIcon;
+  private static Bitmap mBackIcon;
+  private static ImagesLoad mImagesLoader;
+  private FilePickerAdapter mAdapter;
+
+  public GearsFilePickerDialog(Activity activity,
+                               Handler handler,
+                               String arguments) {
+    super (activity, handler, arguments);
+    mAdapter = new FilePickerAdapter(activity);
+  }
+
+  public void setup() {
+    inflate(R.layout.gears_dialog_filepicker, R.id.panel_content);
+    setupButtons(0,
+                 R.string.filepicker_button_allow,
+                 R.string.filepicker_button_deny);
+    setupDialog();
+    GridView view = (GridView) findViewById(R.id.files_list);
+    view.setAdapter(mAdapter);
+    view.setOnTouchListener(this);
+
+    mImagesLoader = new ImagesLoad(mAdapter);
+    mImagesLoader.setAdapterView(view);
+    Thread thread = new Thread(mImagesLoader);
+    thread.start();
+  }
+
+  public void setupDialog(TextView message, ImageView icon) {
+    message.setText(R.string.filepicker_message);
+    message.setTextSize(24);
+    icon.setImageResource(R.drawable.gears_icon_48x48);
+  }
+
+  public boolean onTouch(View v, MotionEvent event) {
+    mImagesLoader.pauseIconRequest();
+    return false;
+  }
+
+  /**
+   * Utility class to load and generate thumbnails
+   * for image files
+   */
+  class ImagesLoad implements Runnable {
+    private Map mImagesMap;
+    private Vector mImagesPath;
+    private BaseAdapter mAdapter;
+    private AdapterView mAdapterView;
+    private Vector<FilePickerElement> mElements;
+    private Handler mLoaderHandler;
+
+    ImagesLoad(BaseAdapter adapter) {
+      mAdapter = adapter;
+    }
+
+    public void signalChanges() {
+      Message message = mHandler.obtainMessage(GearsBaseDialog.NEW_ICON,
+                                               mAdapter);
+      mHandler.sendMessage(message);
+    }
+
+    /**
+     * TODO: use the same thumbnails as the photo app
+     * (bug: http://b/issue?id=1497927)
+     */
+    public String getThumbnailPath(String path) {
+      File f = new File(path);
+      String myPath = f.getParent() + "/.thumbnails";
+      File d = new File(myPath);
+      if (!d.exists()) {
+        d.mkdirs();
+      }
+      return myPath + "/" + f.getName();
+    }
+
+    public boolean saveImage(String path, Bitmap image) {
+      boolean ret = false;
+      try {
+        FileOutputStream outStream = new FileOutputStream(path);
+        ret = image.compress(Bitmap.CompressFormat.JPEG, 100, outStream);
+      } catch (IOException e) {
+        Log.e(TAG, "IOException ", e);
+      }
+      return ret;
+    }
+
+    public Bitmap generateImage(FilePickerElement elem) {
+      String path = elem.getPath();
+      Bitmap finalImage = null;
+      try {
+        String thumbnailPath = getThumbnailPath(path);
+        File thumbnail = new File(thumbnailPath);
+        if (thumbnail.exists()) {
+          finalImage = BitmapFactory.decodeFile(thumbnailPath);
+          if (finalImage != null) {
+            return finalImage;
+          }
+        }
+        BitmapFactory.Options options = new BitmapFactory.Options();
+        options.inJustDecodeBounds = true;
+        BitmapFactory.decodeFile(path, options);
+
+        int width = options.outWidth;
+        int height = options.outHeight;
+        int size = 128;
+        int sampleSize = 1;
+        if (width > size || height > size) {
+          sampleSize = 2;
+          while ((width / sampleSize > size)
+                 || (height / sampleSize > size)) {
+            sampleSize += 2;
+          }
+        }
+        options.inJustDecodeBounds = false;
+        options.inSampleSize = sampleSize;
+        Bitmap originalImage = BitmapFactory.decodeFile(path, options);
+        if (originalImage == null) {
+          return null;
+        }
+        finalImage = Bitmap.createScaledBitmap(originalImage, size, size, true);
+        if (saveImage(thumbnailPath, finalImage)) {
+          if (mDebug) {
+            Log.v(TAG, "Saved thumbnail for file " + path);
+          }
+        } else {
+          Log.e(TAG, "Could NOT Save thumbnail for file " + path);
+        }
+        originalImage.recycle();
+      } catch (java.lang.OutOfMemoryError e) {
+        Log.e(TAG, "Intercepted OOM ", e);
+      }
+      return finalImage;
+    }
+
+    public void pauseIconRequest() {
+      Message message = Message.obtain(mLoaderHandler,
+                                       GearsBaseDialog.PAUSE_REQUEST_ICON);
+      mLoaderHandler.sendMessageAtFrontOfQueue(message);
+    }
+    public void postIconRequest(FilePickerElement item, int position) {
+      if (item == null) {
+        return;
+      }
+      Message message = mLoaderHandler.obtainMessage(
+          GearsBaseDialog.REQUEST_ICON, position, 0, item);
+      mLoaderHandler.sendMessage(message);
+    }
+
+    public void generateIcon(FilePickerElement elem) {
+      if (elem.isImage()) {
+        if (elem.getThumbnail() == null) {
+          Bitmap image = generateImage(elem);
+          if (image != null) {
+            elem.setThumbnail(image);
+          }
+        }
+      }
+    }
+
+    public void setAdapterView(AdapterView view) {
+      mAdapterView = view;
+    }
+
+    public void run() {
+      Looper.prepare();
+      mLoaderHandler = new Handler() {
+        public void handleMessage(Message msg) {
+          int visibleElements = 10;
+          if (msg.what == GearsBaseDialog.PAUSE_REQUEST_ICON) {
+            try {
+              // We are busy (likely) scrolling the view,
+              // so we just pause the loading.
+              Thread.sleep(1000);
+              mLoaderHandler.removeMessages(
+                  GearsBaseDialog.PAUSE_REQUEST_ICON);
+            } catch (InterruptedException e) {
+              Log.e(TAG, "InterruptedException ", e);
+            }
+          } else if (msg.what == GearsBaseDialog.REQUEST_ICON) {
+            try {
+              Thread.sleep(10);
+            } catch (InterruptedException e) {
+              Log.e(TAG, "InterruptedException ", e);
+            }
+            FilePickerElement elem = (FilePickerElement) msg.obj;
+            int firstVisiblePosition = mAdapterView.getFirstVisiblePosition();
+            // If the elements are not visible, we slow down the update
+            // TODO: replace this by a low-priority thread
+            if ((msg.arg1 < firstVisiblePosition - visibleElements)
+                && msg.arg1 > firstVisiblePosition + visibleElements) {
+              try {
+                Thread.sleep(100);
+              } catch (InterruptedException e) {
+              }
+            }
+            generateIcon(elem);
+            signalChanges();
+          }
+        }
+      };
+      Looper.loop();
+    }
+  }
+
+  /**
+   * Utility class representing an element displayed in the
+   * file picker, associated with an icon and/or thumbnail
+   */
+  class FilePickerElement {
+    private File mPath;
+    private String mName;
+    private Bitmap mIcon;
+    private boolean mIsSelected;
+    private Vector mChildren;
+    private FilePickerElement mParent;
+    private boolean mIsParent;
+    private BaseAdapter mAdapter;
+    private String mExtension;
+    private Bitmap mThumbnail;
+    private boolean mIsImage;
+
+    public FilePickerElement(String name, BaseAdapter adapter) {
+      this(name, adapter, null);
+    }
+
+    public FilePickerElement(String path, String name, BaseAdapter adapter) {
+      this(path, name, adapter, null);
+    }
+
+    public FilePickerElement(String name,
+                             BaseAdapter adapter,
+                             FilePickerElement parent) {
+      mName = name;
+      mAdapter = adapter;
+      mParent = parent;
+      mIsSelected = false;
+      mChildren = null;
+    }
+
+    public FilePickerElement(String path,
+                             String name,
+                             BaseAdapter adapter,
+                             FilePickerElement parent) {
+      mPath = new File(path);
+      mName = name;
+      mIsSelected = false;
+      mChildren = null;
+      mParent = parent;
+      mAdapter = adapter;
+      mExtension = null;
+
+      setIcons();
+    }
+
+    public void setIcons() {
+      if (mPath.isDirectory()) {
+        if (mDirectoryIcon == null) {
+          mDirectoryIcon = BitmapFactory.decodeResource(
+              getResources(), R.drawable.gears_folder);
+        }
+        mIcon = mDirectoryIcon;
+
+      } else {
+        if (isImage()) {
+          if (mImageIcon == null) {
+            mImageIcon = BitmapFactory.decodeResource(
+                getResources(), R.drawable.gears_file_image);
+          }
+          mIcon = mImageIcon;
+        } else if (isAudio()) {
+          mIcon = BitmapFactory.decodeResource(
+              getResources(), R.drawable.gears_file_audio);
+        } else if (isVideo()) {
+          mIcon = BitmapFactory.decodeResource(
+              getResources(), R.drawable.gears_file_video);
+        } else {
+          if (mDefaultIcon == null) {
+            mDefaultIcon = BitmapFactory.decodeResource(
+                getResources(), R.drawable.gears_file_unknown);
+          }
+          mIcon = mDefaultIcon;
+        }
+      }
+      if (mBackIcon == null) {
+        mBackIcon = BitmapFactory.decodeResource(getResources(),
+                                                 R.drawable.gears_back);
+      }
+    }
+
+    public boolean isImage() {
+      if (mIsImage) return mIsImage;
+      String extension = getExtension();
+      if (extension != null) {
+        if (extension.equalsIgnoreCase("jpg") ||
+            extension.equalsIgnoreCase("jpeg") ||
+            extension.equalsIgnoreCase("png") ||
+            extension.equalsIgnoreCase("gif")) {
+          mIsImage = true;
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public boolean isAudio() {
+      String extension = getExtension();
+      if (extension != null) {
+        if (extension.equalsIgnoreCase("mp3") ||
+            extension.equalsIgnoreCase("wav") ||
+            extension.equalsIgnoreCase("aac")) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public boolean isVideo() {
+      String extension = getExtension();
+      if (extension != null) {
+        if (extension.equalsIgnoreCase("mpg") ||
+            extension.equalsIgnoreCase("mpeg") ||
+            extension.equalsIgnoreCase("mpe") ||
+            extension.equalsIgnoreCase("divx") ||
+            extension.equalsIgnoreCase("3gpp") ||
+            extension.equalsIgnoreCase("avi")) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    public void setParent(boolean isParent) {
+      mIsParent = isParent;
+    }
+
+    public boolean isDirectory() {
+      return mPath.isDirectory();
+    }
+
+    public String getExtension() {
+      if (isDirectory()) {
+        return null;
+      }
+      if (mExtension == null) {
+        String path = getPath();
+        int index = path.lastIndexOf(".");
+        if ((index != -1) && (index != path.length() - 1)){
+          // if we find a dot that is not the last character
+          mExtension = path.substring(index+1);
+          return mExtension;
+        }
+      }
+      return mExtension;
+    }
+
+    public void refresh() {
+      mChildren = null;
+      Vector children = getChildren();
+      for (int i = 0; i < children.size(); i++) {
+        FilePickerElement elem = (FilePickerElement) children.get(i);
+        mImagesLoader.postIconRequest(elem, i);
+      }
+    }
+
+    public Vector getChildren() {
+      if (isDirectory()) {
+        if (mChildren == null) {
+          mChildren = new Vector();
+          File[] files = mPath.listFiles();
+          if (mParent != null) {
+            mChildren.add(mParent);
+            mParent.setParent(true);
+          }
+          for (int i = 0; i < files.length; i++) {
+            String name = files[i].getName();
+            String fpath = files[i].getPath();
+            if (!name.startsWith(".")) { // hide dotfiles
+              FilePickerElement elem = new FilePickerElement(fpath, name,
+                                                             mAdapter, this);
+              elem.setParent(false);
+              mChildren.add(elem);
+            }
+          }
+        }
+      }
+      return mChildren;
+    }
+
+    public FilePickerElement getChild(int position) {
+      Vector children = getChildren();
+      if (children != null) {
+        return (FilePickerElement) children.get(position);
+      }
+      return null;
+    }
+
+    public Bitmap getIcon(int position) {
+      if (mIsParent) {
+        return mBackIcon;
+      }
+      if (isImage()) {
+        if (mThumbnail != null) {
+          return mThumbnail;
+        } else {
+          mImagesLoader.postIconRequest(this, position);
+        }
+      }
+      return mIcon;
+    }
+
+    public Bitmap getThumbnail() {
+      return mThumbnail;
+    }
+
+    public void setThumbnail(Bitmap icon) {
+      mThumbnail = icon;
+    }
+
+    public String getName() {
+      return mName;
+    }
+
+    public String getPath() {
+      return mPath.getPath();
+    }
+
+    public void toggleSelection() {
+      mIsSelected = !mIsSelected;
+    }
+
+    public boolean isSelected() {
+      return mIsSelected;
+    }
+
+  }
+
+  /**
+   * Adapter for the GridView
+   */
+  class FilePickerAdapter extends BaseAdapter {
+    private Context mContext;
+    private Map mImagesMap;
+    private Map mImagesSelected;
+
+    private Vector mImages;
+    private Vector<FilePickerElement> mFiles;
+
+    private FilePickerElement mRootElement;
+    private FilePickerElement mCurrentElement;
+
+    public FilePickerAdapter(Context context) {
+      mContext = context;
+      mImages = new Vector();
+      mFiles = new Vector();
+
+      mImagesMap = Collections.synchronizedMap(new HashMap());
+      mImagesSelected = new HashMap();
+
+      Uri requests[] = { MediaStore.Images.Media.INTERNAL_CONTENT_URI,
+                         MediaStore.Images.Media.EXTERNAL_CONTENT_URI };
+
+      String sdCardPath = Environment.getExternalStorageDirectory().getPath();
+      mRootElement = new FilePickerElement(sdCardPath, "SD Card", this);
+      mCurrentElement = mRootElement;
+    }
+
+    public void addImage(String path) {
+      mImages.add(path);
+      Bitmap image = BitmapFactory.decodeResource(
+          getResources(), R.drawable.gears_file_unknown);
+      mImagesMap.put(path, image);
+      mImagesSelected.put(path, Boolean.FALSE);
+    }
+
+    public int getCount() {
+      Vector elems = mCurrentElement.getChildren();
+      return elems.size();
+    }
+
+    public Object getItem(int position) {
+      return position;
+    }
+
+    public long getItemId(int position) {
+      return position;
+    }
+
+    public Vector selectedElements() {
+      if (mCurrentElement == null) {
+        return null;
+      }
+      Vector children = mCurrentElement.getChildren();
+      Vector ret = new Vector();
+      for (int i = 0; i < children.size(); i++) {
+        FilePickerElement elem = (FilePickerElement) children.get(i);
+        if (elem.isSelected()) {
+          ret.add(elem);
+        }
+      }
+      return ret;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+      View cell = convertView;
+      if (cell == null) {
+        LayoutInflater inflater = (LayoutInflater) getSystemService(
+            Context.LAYOUT_INFLATER_SERVICE);
+        cell = inflater.inflate(R.layout.gears_dialog_filepicker_cell, null);
+      }
+      ImageView imageView = (ImageView) cell.findViewById(R.id.icon);
+      TextView textView = (TextView) cell.findViewById(R.id.name);
+      FilePickerElement elem = mCurrentElement.getChild(position);
+      if (elem == null) {
+        String message = "Could not get elem " + position;
+        message += " for " + mCurrentElement.getPath();
+        Log.e(TAG, message);
+        return null;
+      }
+      String path = elem.getPath();
+      textView.setText(elem.getName());
+
+      View.OnClickListener listener = new View.OnClickListener() {
+        public void onClick(View view) {
+          int pos = (Integer) view.getTag();
+          FilePickerElement elem = mCurrentElement.getChild(pos);
+          if (elem.isDirectory()) {
+            mCurrentElement = elem;
+            mCurrentElement.refresh();
+          } else {
+            elem.toggleSelection();
+          }
+          notifyDataSetChanged();
+        }
+      };
+      imageView.setOnClickListener(listener);
+      cell.setLayoutParams(new GridView.LayoutParams(96, 96));
+
+      imageView.setTag(position);
+
+      if (elem.isSelected()) {
+        cell.setBackgroundColor(Color.LTGRAY);
+      } else {
+        cell.setBackgroundColor(Color.WHITE);
+      }
+      Bitmap bmp = elem.getIcon(position);
+      if (bmp != null) {
+        imageView.setImageBitmap(bmp);
+      }
+
+      return cell;
+    }
+  }
+
+  private String selectedFiles() {
+    Vector selection = mAdapter.selectedElements();
+    JSONArray jsonSelection = new JSONArray();
+    if (selection != null) {
+      for (int i = 0; i < selection.size(); i++) {
+        FilePickerElement elem = (FilePickerElement) selection.get(i);
+        jsonSelection.put(elem.getPath());
+      }
+    }
+    return jsonSelection.toString();
+  }
+
+  public String closeDialog(int closingType) {
+    return selectedFiles();
+  }
+}
diff --git a/src/com/android/browser/GearsNativeDialog.java b/src/com/android/browser/GearsNativeDialog.java
new file mode 100644
index 0000000..c8ae741
--- /dev/null
+++ b/src/com/android/browser/GearsNativeDialog.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Config;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Window;
+import android.widget.BaseAdapter;
+
+import android.webkit.gears.NativeDialog;
+
+import com.android.browser.GearsBaseDialog;
+import com.android.browser.GearsPermissionsDialog;
+import com.android.browser.GearsSettingsDialog;
+import com.android.browser.GearsShortcutDialog;
+import com.android.browser.GearsFilePickerDialog;
+
+/**
+ * Native dialog Activity used by gears
+ * TODO: rename in GearsNativeDialogActivity
+ * @hide
+ */
+public class GearsNativeDialog extends Activity {
+
+  private static final String TAG = "GearsNativeDialog";
+
+  private String mDialogArguments;
+
+  private String mGearsVersion = null;
+
+  private boolean mDebug = false;
+
+  private int mDialogType;
+  private final int SETTINGS_DIALOG = 1;
+  private final int PERMISSION_DIALOG = 2;
+  private final int SHORTCUT_DIALOG = 3;
+  private final int LOCATION_DIALOG = 4;
+  private final int FILEPICKER_DIALOG = 5;
+
+  private final String VERSION_STRING = "version";
+  private final String SETTINGS_DIALOG_STRING = "settings_dialog";
+  private final String PERMISSION_DIALOG_STRING = "permissions_dialog";
+  private final String SHORTCUT_DIALOG_STRING = "shortcuts_dialog";
+  private final String LOCATION_DIALOG_STRING = "locations_dialog";
+  private final String FILEPICKER_DIALOG_STRING = "filepicker_dialog";
+
+  private boolean mDialogDismissed = false;
+
+  GearsBaseDialog dialog;
+
+  // Handler for callbacks to the UI thread
+  final Handler mHandler = new Handler() {
+    public void handleMessage(Message msg) {
+      if (msg.what == GearsBaseDialog.NEW_ICON) {
+        BaseAdapter adapter = (BaseAdapter) msg.obj;
+        adapter.notifyDataSetChanged();
+      } else if (msg.what == GearsBaseDialog.UPDATE_ICON) {
+        dialog.updateIcon();
+      } else if (msg.what == GearsBaseDialog.ALWAYS_DENY) {
+        closeDialog(GearsBaseDialog.ALWAYS_DENY);
+      } else if (msg.what == GearsBaseDialog.ALLOW) {
+        closeDialog(GearsBaseDialog.ALLOW);
+      } else if (msg.what == GearsBaseDialog.DENY) {
+        closeDialog(GearsBaseDialog.DENY);
+      }
+      super.handleMessage(msg);
+    }
+  };
+
+  @Override
+  public void onCreate(Bundle icicle) {
+    super.onCreate(icicle);
+    requestWindowFeature(Window.FEATURE_NO_TITLE);
+    setContentView(R.layout.gears_dialog);
+    getArguments();
+
+    switch (mDialogType) {
+      case SETTINGS_DIALOG:
+        dialog = new GearsSettingsDialog(this, mHandler, mDialogArguments);
+        dialog.setGearsVersion(mGearsVersion);
+        break;
+      case PERMISSION_DIALOG:
+        dialog = new GearsPermissionsDialog(this, mHandler, mDialogArguments);
+        break;
+      case SHORTCUT_DIALOG:
+        dialog = new GearsShortcutDialog(this, mHandler, mDialogArguments);
+        break;
+      case LOCATION_DIALOG:
+        dialog = new GearsPermissionsDialog(this, mHandler, mDialogArguments);
+        break;
+      case FILEPICKER_DIALOG:
+        dialog = new GearsFilePickerDialog(this, mHandler, mDialogArguments);
+        break;
+      default:
+        dialog = new GearsBaseDialog(this, mHandler, mDialogArguments);
+    }
+    dialog.setDebug(mDebug);
+    dialog.setup();
+  }
+
+  /**
+   * Get the arguments for the dialog
+   *
+   * The dialog needs a json string as an argument, as
+   * well as a dialogType. In debug mode the arguments
+   * are mocked.
+   */
+  private void getArguments() {
+    if (mDebug) {
+      mDialogType = FILEPICKER_DIALOG +1;
+      mockArguments();
+
+      return;
+    }
+
+    Intent intent = getIntent();
+    mDialogArguments = intent.getStringExtra("dialogArguments");
+    String dialogTypeString = intent.getStringExtra("dialogType");
+    if (dialogTypeString == null) {
+      return;
+    }
+
+    if (Config.LOGV) {
+      Log.v(TAG, "dialogtype: " + dialogTypeString);
+    }
+
+    if (dialogTypeString.equalsIgnoreCase(SETTINGS_DIALOG_STRING)) {
+      mDialogType = SETTINGS_DIALOG;
+      mGearsVersion = intent.getStringExtra(VERSION_STRING);
+    } else if (dialogTypeString.equalsIgnoreCase(PERMISSION_DIALOG_STRING)) {
+      mDialogType = PERMISSION_DIALOG;
+    } else if (dialogTypeString.equalsIgnoreCase(SHORTCUT_DIALOG_STRING)) {
+      mDialogType = SHORTCUT_DIALOG;
+    } else if (dialogTypeString.equalsIgnoreCase(LOCATION_DIALOG_STRING)) {
+      mDialogType = LOCATION_DIALOG;
+    } else if (dialogTypeString.equalsIgnoreCase(FILEPICKER_DIALOG_STRING)) {
+      mDialogType = FILEPICKER_DIALOG;
+    }
+  }
+
+  /**
+   * Utility method for debugging the dialog.
+   *
+   * Set mock arguments.
+   */
+  private void mockArguments() {
+    String argumentsShortcuts = "{ locale: \"en-US\","
+        + "name: \"My Application\", link: \"http://www.google.com/\","
+        + "description: \"This application does things does things!\","
+        + "icon16x16: \"http://google-gears.googlecode.com/"
+        + "svn/trunk/gears/test/manual/shortcuts/16.png\","
+        + "icon32x32: \"http://google-gears.googlecode.com/"
+        + "svn/trunk/gears/test/manual/shortcuts/32.png\","
+        + "icon48x48: \"http://google-gears.googlecode.com/"
+        + "svn/trunk/gears/test/manual/shortcuts/48.png\","
+        + "icon128x128: \"http://google-gears.googlecode.com/"
+        + "svn/trunk/gears/test/manual/shortcuts/128.png\"}";
+
+    String argumentsPermissions = "{ locale: \"en-US\", "
+        + "origin: \"http://www.google.com\", dialogType: \"localData\","
+        + "customIcon: \"http://google-gears.googlecode.com/"
+        + "svn/trunk/gears/test/manual/shortcuts/32.png\","
+        + "customName: \"My Application\","
+        + "customMessage: \"Press the button to enable my "
+        + "application to run offline!\" };";
+
+    String argumentsLocation = "{ locale: \"en-US\", "
+        + "origin: \"http://www.google.com\", dialogType: \"locationData\","
+        + "customIcon: \"http://google-gears.googlecode.com/"
+        + "svn/trunk/gears/test/manual/shortcuts/32.png\","
+        + "customName: \"My Application\","
+        + "customMessage: \"Press the button to enable my "
+        + "application to run offline!\" };";
+
+    String argumentsSettings = "{ locale: \"en-US\", permissions: [ { "
+        + "name: \"http://www.google.com\", "
+        + "localStorage: { permissionState: 1 }, "
+        + "locationData: { permissionState: 0 } }, "
+        + "{ name: \"http://www.aaronboodman.com\", "
+        + "localStorage: { permissionState: 1 }, "
+        + "locationData: { permissionState: 2 } }, "
+        + "{ name: \"http://www.evil.org\", "
+        + "localStorage: { permissionState: 2 }, "
+        + "locationData: { permissionState: 2 } } ] }";
+
+    switch (mDialogType) {
+      case SHORTCUT_DIALOG:
+        mDialogArguments = argumentsShortcuts;
+        break;
+      case PERMISSION_DIALOG:
+        mDialogArguments = argumentsPermissions;
+        break;
+      case LOCATION_DIALOG:
+        mDialogArguments = argumentsLocation;
+        break;
+      case SETTINGS_DIALOG:
+        mDialogArguments = argumentsSettings;
+    }
+  }
+
+  /**
+   * Close the dialog and set the return string value.
+   */
+  private void closeDialog(int closingType) {
+    String ret = dialog.closeDialog(closingType);
+
+    if (mDebug) {
+      Log.v(TAG, "closeDialog ret value: " + ret);
+    }
+
+    NativeDialog.closeDialog(ret);
+    notifyEndOfDialog();
+    finish();
+  }
+
+  @Override
+  public void onDestroy() {
+    super.onDestroy();
+    // In case we reach this point without
+    // notifying NativeDialog, we do it now.
+    if (!mDialogDismissed) {
+      notifyEndOfDialog();
+    }
+  }
+
+  @Override
+  public void onPause(){
+    super.onPause();
+    if (!mDialogDismissed) {
+      closeDialog(GearsBaseDialog.CANCEL);
+    }
+  }
+
+  /**
+   * Signal to NativeDialog that we are done.
+   */
+  private void notifyEndOfDialog() {
+    NativeDialog.signalFinishedDialog();
+    mDialogDismissed = true;
+  }
+
+  /**
+   * Intercepts the back key to immediately notify
+   * NativeDialog that we are done.
+   */
+  public boolean dispatchKeyEvent(KeyEvent event) {
+    if (event.getKeyCode() ==  KeyEvent.KEYCODE_BACK && event.isDown()) {
+      closeDialog(GearsBaseDialog.CANCEL);
+    }
+    return super.dispatchKeyEvent(event);
+  }
+
+}
diff --git a/src/com/android/browser/GearsPermissions.java b/src/com/android/browser/GearsPermissions.java
new file mode 100644
index 0000000..cd46324
--- /dev/null
+++ b/src/com/android/browser/GearsPermissions.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * The permission mechanism works the following way:
+ *
+ * PermissionType allows to define a type of permission
+ *   (e.g. localStorage/locationData), storing a name and a set of
+ *   resource ids corresponding to the GUI resources.
+ *
+ * Permission defines an actual permission instance, with a type and a value.
+ *
+ * OriginPermissions holds an origin with a set of Permission objects
+ */
+class GearsPermissions {
+
+  private static final String TAG = "GearsPermissions";
+
+  /**
+   * Defines a type of permission
+   *
+   * Store the permission's name (used in the json result)
+   * Graphically, each permission is a label followed by two radio buttons.
+   * We store the resources ids here.
+   */
+  public static class PermissionType {
+    public static final int PERMISSION_NOT_SET = 0;
+    public static final int PERMISSION_ALLOWED = 1;
+    public static final int PERMISSION_DENIED = 2;
+
+    String mName;
+    int mRowRsc;
+    int mAllowedButtonRsc;
+    int mDeniedButtonRsc;
+
+    PermissionType(String name) {
+      mName = name;
+    }
+
+    public void setResources(int rowRsc, int allowedButtonRsc,
+        int deniedButtonRsc) {
+      mRowRsc = rowRsc;
+      mAllowedButtonRsc = allowedButtonRsc;
+      mDeniedButtonRsc = deniedButtonRsc;
+    }
+
+    public int getRowRsc() {
+      return mRowRsc;
+    }
+
+    public int getAllowedButtonRsc() {
+      return mAllowedButtonRsc;
+    }
+
+    public int getDeniedButtonRsc() {
+      return mDeniedButtonRsc;
+    }
+
+    public String getName() {
+      return mName;
+    }
+
+  }
+
+  /**
+   * Simple class to store an instance of a permission
+   *
+   * i.e. a permission type and a value
+   * Value can be either PERMISSION_NOT_SET,
+   * PERMISSION_ALLOWED or PERMISSION_DENIED
+   * (defined in PermissionType).
+   */
+  public static class Permission {
+    PermissionType mType;
+    int mValue;
+
+    Permission(PermissionType type, int value) {
+      mType = type;
+      mValue = value;
+    }
+
+    Permission(PermissionType type) {
+      mType = type;
+      mValue = 0;
+    }
+
+    public PermissionType getType() {
+      return mType;
+    }
+
+    public void setValue(int value) {
+      mValue = value;
+    }
+
+    public int getValue() {
+      return mValue;
+    }
+  }
+
+  /**
+   * Interface used by the GearsNativeDialog implementation
+   * to listen to changes in the permissions.
+   */
+  public interface PermissionsChangesListener {
+    public boolean setPermission(PermissionType type, int perm);
+  }
+
+  /**
+   * Holds the model for an origin -- each origin has a set of
+   * permissions.
+   */
+  public static class OriginPermissions {
+    HashMap<PermissionType, Permission> mPermissions;
+    String mOrigin;
+    public static PermissionsChangesListener mListener;
+
+    public static void setListener(PermissionsChangesListener listener) {
+      mListener = listener;
+    }
+
+    OriginPermissions(String anOrigin) {
+      mOrigin = anOrigin;
+      mPermissions = new HashMap<PermissionType, Permission>();
+    }
+
+    OriginPermissions(OriginPermissions perms) {
+      mOrigin = perms.getOrigin();
+      mPermissions = new HashMap<PermissionType, Permission>();
+      HashMap<PermissionType, Permission> permissions = perms.getPermissions();
+      Iterator<PermissionType> iterator = permissions.keySet().iterator();
+      while (iterator.hasNext()) {
+        Permission permission = permissions.get(iterator.next());
+        int value = permission.getValue();
+        setPermission(permission.getType(), value);
+      }
+    }
+
+    public String getOrigin() {
+      return mOrigin;
+    }
+
+    public HashMap<PermissionType, Permission> getPermissions() {
+      return mPermissions;
+    }
+
+    public int getPermission(PermissionType type) {
+      return mPermissions.get(type).getValue();
+    }
+
+    public void setPermission(PermissionType type, int perm) {
+      if (mPermissions.get(type) == null) {
+        Permission permission = new Permission(type, perm);
+        mPermissions.put(type, permission);
+        return;
+      }
+
+      if (mListener != null) {
+        mListener.setPermission(type, perm);
+      }
+
+      mPermissions.get(type).setValue(perm);
+    }
+
+    public void print() {
+      Log.v(TAG, "Permissions for " + mOrigin);
+      Iterator<PermissionType> iterator = mPermissions.keySet().iterator();
+      while (iterator.hasNext()) {
+        Permission permission = mPermissions.get(iterator.next());
+        String name = permission.getType().getName();
+        int value = permission.getValue();
+        Log.v(TAG, "  " + name + ": " + value);
+      }
+    }
+  }
+
+}
diff --git a/src/com/android/browser/GearsPermissionsDialog.java b/src/com/android/browser/GearsPermissionsDialog.java
new file mode 100644
index 0000000..b57ab0b
--- /dev/null
+++ b/src/com/android/browser/GearsPermissionsDialog.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Gears permission dialog
+ */
+class GearsPermissionsDialog extends GearsBaseDialog {
+
+  private static final String TAG = "GearsPermissionsDialog";
+
+  private String mDialogType;
+
+  public GearsPermissionsDialog(Activity activity,
+                                Handler handler,
+                                String arguments) {
+    super (activity, handler, arguments);
+  }
+
+  public void setup() {
+    inflate(R.layout.gears_dialog_permission, R.id.panel_content);
+    setupButtons(R.string.permission_button_alwaysdeny,
+                 R.string.permission_button_allow,
+                 R.string.permission_button_deny);
+
+    View contentBorder = findViewById(R.id.content_border);
+    if (contentBorder != null) {
+      contentBorder.setBackgroundResource(R.color.permission_border);
+    }
+    View contentBackground = findViewById(R.id.content_background);
+    if (contentBackground != null) {
+      contentBackground.setBackgroundResource(R.color.permission_background);
+    }
+
+    try {
+      JSONObject json = new JSONObject(mDialogArguments);
+
+      if (json.has("dialogType")) {
+        mDialogType = json.getString("dialogType");
+        setupDialog();
+      }
+
+      if (!json.has("customName")) {
+        setLabel(json, "origin", R.id.origin_title);
+        View titleView = findViewById(R.id.origin_title);
+        if (titleView != null) {
+          TextView title = (TextView) titleView;
+          title.setGravity(Gravity.CENTER);
+        }
+      } else {
+        setLabel(json, "customName", R.id.origin_title);
+        setLabel(json, "origin", R.id.origin_subtitle);
+        setLabel(json, "customMessage", R.id.origin_message);
+      }
+
+      if (json.has("customIcon")) {
+        String iconUrl = json.getString("customIcon");
+        mChoosenIconSize = 32;
+        downloadIcon(iconUrl);
+      }
+
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON exception ", e);
+    }
+  }
+
+  public void setupDialog(TextView message, ImageView icon) {
+    if (mDialogType.equalsIgnoreCase(LOCAL_DATA_STRING)) {
+      message.setText(R.string.query_data_message);
+      icon.setImageResource(R.drawable.gears_local_data);
+    } else if (mDialogType.equalsIgnoreCase(LOCATION_DATA_STRING)) {
+      message.setText(R.string.location_message);
+      icon.setImageResource(R.drawable.gears_location_data);
+      View privacyPolicyLabel = findViewById(R.id.privacy_policy_label);
+      if (privacyPolicyLabel != null) {
+        privacyPolicyLabel.setVisibility(View.VISIBLE);
+      }
+    }
+  }
+
+  public String closeDialog(int closingType) {
+    String ret = null;
+    switch (closingType) {
+      case ALWAYS_DENY:
+        ret = "{\"allow\": false, \"permanently\": true }";
+        break;
+      case ALLOW:
+        ret = "{\"allow\": true, \"permanently\": true }";
+        break;
+      case DENY:
+        ret = "{\"allow\": false, \"permanently\": false }";
+        break;
+    }
+    return ret;
+  }
+
+}
diff --git a/src/com/android/browser/GearsSettingsDialog.java b/src/com/android/browser/GearsSettingsDialog.java
new file mode 100644
index 0000000..dead4f2
--- /dev/null
+++ b/src/com/android/browser/GearsSettingsDialog.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.content.Context;
+import android.os.Handler;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.RadioButton;
+import android.widget.TextView;
+
+import com.android.browser.GearsPermissions.OriginPermissions;
+import com.android.browser.GearsPermissions.PermissionsChangesListener;
+import com.android.browser.GearsPermissions.PermissionType;
+
+import java.util.Vector;
+import java.util.List;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Gears Settings dialog
+ */
+class GearsSettingsDialog extends GearsBaseDialog
+    implements PermissionsChangesListener {
+
+  private static final String TAG = "GearsPermissionsDialog";
+  private Vector<OriginPermissions> mSitesPermissions = null;
+  private Vector<OriginPermissions> mOriginalPermissions = null;
+  private Vector<OriginPermissions> mCurrentPermissions = null;
+
+  private Vector<PermissionType> mPermissions;
+
+  // We declare the permissions globally to simplify the code
+  private final PermissionType LOCAL_STORAGE =
+      new PermissionType(LOCAL_STORAGE_STRING);
+  private final PermissionType LOCATION_DATA =
+      new PermissionType(LOCATION_DATA_STRING);
+
+  private boolean mChanges = false;
+
+
+  public GearsSettingsDialog(Activity activity,
+                             Handler handler,
+                             String arguments) {
+    super (activity, handler, arguments);
+  }
+
+  public void setup() {
+    // First let's add the permissions' resources
+    LOCAL_STORAGE.setResources(R.id.local_storage_choice,
+       R.id.local_storage_allowed,
+       R.id.local_storage_denied);
+
+    LOCATION_DATA.setResources(R.id.location_data_choice,
+       R.id.location_data_allowed,
+       R.id.location_data_denied);
+
+    // add the permissions to the list of permissions.
+    mPermissions = new Vector<PermissionType>();
+    mPermissions.add(LOCAL_STORAGE);
+    mPermissions.add(LOCATION_DATA);
+    OriginPermissions.setListener(this);
+
+
+    inflate(R.layout.gears_dialog_settings, R.id.panel_content);
+    setupDialog();
+    setupButtons(0,
+                 R.string.settings_button_allow,
+                 R.string.settings_button_deny);
+
+    // by default disable the allow button (it will get enabled if
+    // something is changed by the user)
+    View buttonView = findViewById(R.id.button_allow);
+    if (buttonView != null) {
+      Button button = (Button) buttonView;
+      button.setEnabled(false);
+    }
+
+    View gearsVersionView = findViewById(R.id.gears_version);
+    if (gearsVersionView != null) {
+      TextView gearsVersion = (TextView) gearsVersionView;
+      gearsVersion.setText(mGearsVersion);
+    }
+
+    // We manage the permissions using three vectors, mSitesPermissions,
+    // mOriginalPermissions and mCurrentPermissions.
+    // The dialog's arguments are parsed and a list of permissions is
+    // generated and stored in those three vectors.
+    // mOriginalPermissions is a separate copy and will not be modified;
+    // mSitesPermissions contains the current permissions _only_ --
+    // if an origin is removed, it is also removed from mSitesPermissions.
+    // Finally, mCurrentPermissions contains the current permissions and
+    // is a clone of mSitesPermissions, but removed sites aren't removed,
+    // their permissions are simply set to PERMISSION_NOT_SET. This
+    // allows us to easily generate the final difference between the
+    // original permissions and the final permissions, while directly
+    // using mSitesPermissions for the listView adapter (SettingsAdapter).
+
+    mSitesPermissions = new Vector<OriginPermissions>();
+    mOriginalPermissions = new Vector<OriginPermissions>();
+
+    try {
+      JSONObject json = new JSONObject(mDialogArguments);
+      if (json.has("permissions")) {
+        JSONArray jsonArray = json.getJSONArray("permissions");
+        for (int i = 0; i < jsonArray.length(); i++) {
+          JSONObject infos = jsonArray.getJSONObject(i);
+          String name = null;
+          int localStorage = PermissionType.PERMISSION_NOT_SET;
+          int locationData = PermissionType.PERMISSION_NOT_SET;
+          if (infos.has("name")) {
+            name = infos.getString("name");
+          }
+          if (infos.has(LOCAL_STORAGE_STRING)) {
+            JSONObject perm = infos.getJSONObject(LOCAL_STORAGE_STRING);
+            if (perm.has("permissionState")) {
+              localStorage = perm.getInt("permissionState");
+            }
+          }
+          if (infos.has(LOCATION_DATA_STRING)) {
+            JSONObject perm = infos.getJSONObject(LOCATION_DATA_STRING);
+            if (perm.has("permissionState")) {
+              locationData = perm.getInt("permissionState");
+            }
+          }
+          OriginPermissions perms = new OriginPermissions(name);
+          perms.setPermission(LOCAL_STORAGE, localStorage);
+          perms.setPermission(LOCATION_DATA, locationData);
+
+          mSitesPermissions.add(perms);
+          mOriginalPermissions.add(new OriginPermissions(perms));
+        }
+      }
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON exception ", e);
+    }
+    mCurrentPermissions = (Vector<OriginPermissions>)mSitesPermissions.clone();
+
+    View listView = findViewById(R.id.sites_list);
+    if (listView != null) {
+      ListView list = (ListView) listView;
+      list.setAdapter(new SettingsAdapter(mActivity, mSitesPermissions));
+    }
+    if (mDebug) {
+      printPermissions();
+    }
+  }
+
+  public void setupDialog() {
+    View dialogTitleView = findViewById(R.id.dialog_title);
+    if (dialogTitleView != null) {
+      TextView dialogTitle = (TextView) dialogTitleView;
+      dialogTitle.setText(R.string.settings_title);
+      dialogTitle.setVisibility(View.VISIBLE);
+    }
+    View dialogSubtitleView = findViewById(R.id.dialog_subtitle);
+    if (dialogSubtitleView != null) {
+      TextView dialogSubtitle = (TextView) dialogSubtitleView;
+      dialogSubtitle.setText(R.string.settings_message);
+      dialogSubtitle.setVisibility(View.VISIBLE);
+    }
+    View iconView = findViewById(R.id.icon);
+    if (iconView != null) {
+      ImageView icon = (ImageView) iconView;
+      icon.setImageResource(R.drawable.gears_icon_32x32);
+    }
+  }
+
+  /**
+   * GearsPermissions.PermissionsChangesListener delegate
+   */
+  public boolean setPermission(PermissionType type, int perm) {
+    if (mChanges == false) {
+      signalChanges();
+    }
+    return mChanges;
+  }
+
+  /**
+   * Controller class for binding the model (OriginPermissions) with
+   * the UI.
+   */
+  class PermissionController {
+    final static int ALLOWED_BUTTON = 1;
+    final static int DENIED_BUTTON = 2;
+    private int mButtonType;
+    private PermissionType mPermissionType;
+    private OriginPermissions mPermissions;
+
+    PermissionController(PermissionType permissionType, int buttonType,
+        OriginPermissions permissions) {
+      mPermissionType = permissionType;
+      mButtonType = buttonType;
+      mPermissions = permissions;
+    }
+
+    public boolean isChecked() {
+      boolean checked = false;
+
+      switch (mButtonType) {
+        case ALLOWED_BUTTON:
+          if (mPermissions.getPermission(mPermissionType) ==
+              PermissionType.PERMISSION_ALLOWED) {
+            checked = true;
+          } break;
+        case DENIED_BUTTON:
+          if (mPermissions.getPermission(mPermissionType) ==
+              PermissionType.PERMISSION_DENIED) {
+            checked = true;
+          }
+      }
+      return checked;
+    }
+
+    public String print() {
+        return printType() + " for " + mPermissions.getOrigin();
+    }
+
+    private String printType() {
+      switch (mButtonType) {
+        case ALLOWED_BUTTON:
+          return "ALLOWED_BUTTON";
+        case DENIED_BUTTON:
+          return "DENIED_BUTTON";
+      }
+      return "UNKNOWN BUTTON";
+    }
+
+    public void changed(boolean isChecked) {
+      if (isChecked == isChecked()) {
+        return; // already set
+      }
+
+      switch (mButtonType) {
+        case ALLOWED_BUTTON:
+          mPermissions.setPermission(mPermissionType,
+              PermissionType.PERMISSION_ALLOWED);
+          break;
+        case DENIED_BUTTON:
+          mPermissions.setPermission(mPermissionType,
+              PermissionType.PERMISSION_DENIED);
+          break;
+      }
+    }
+  }
+
+
+
+  /**
+   * Adapter class for the list view in the settings dialog
+   *
+   * Every row in the settings dialog display the permissions
+   * for a given origin. For every type of permission
+   * (location, local data...) there is two radio buttons to
+   * authorize or deny the permission.
+   * A remove button is also present to let the user remove
+   * all the authorization of an origin in one step.
+   */
+  class SettingsAdapter extends ArrayAdapter {
+    private Activity mContext;
+    private List mItems;
+
+    SettingsAdapter(Activity context, List items) {
+      super(context, R.layout.gears_dialog_settings_row, items);
+      mContext = context;
+      mItems = items;
+    }
+
+    /*
+     * setup the necessary listeners for the radiobuttons
+     * When the buttons are clicked the permissions change.
+     */
+    private void createAndSetButtonListener(View buttonView,
+        OriginPermissions perms, PermissionType permissionType,
+        int buttonType) {
+      if (buttonView == null) {
+        return;
+      }
+      RadioButton button = (RadioButton) buttonView;
+
+      button.setOnCheckedChangeListener(null);
+      PermissionController p = new PermissionController(permissionType,
+          buttonType, perms);
+      button.setTag(p);
+
+      CompoundButton.OnCheckedChangeListener listener =
+          new CompoundButton.OnCheckedChangeListener() {
+        public void onCheckedChanged(CompoundButton buttonView,
+            boolean isChecked) {
+          PermissionController perm = (PermissionController)buttonView.getTag();
+          perm.changed(isChecked);
+        }
+      };
+
+      button.setOnCheckedChangeListener(listener);
+
+      if (p.isChecked() != button.isChecked()) {
+        button.setChecked(p.isChecked());
+      }
+    }
+
+    /*
+     * setup the remove button for an origin: each row has a global
+     * remove button in addition to the radio buttons controlling the
+     * permissions.
+     */
+    private void setRemoveButton(Button button, OriginPermissions perms) {
+      Button.OnClickListener listener = new Button.OnClickListener() {
+        public void onClick(View buttonView) {
+          if (mChanges == false) {
+            signalChanges();
+          }
+          OriginPermissions perm = (OriginPermissions) buttonView.getTag();
+          perm.setPermission(LOCAL_STORAGE, PermissionType.PERMISSION_NOT_SET);
+          perm.setPermission(LOCATION_DATA, PermissionType.PERMISSION_NOT_SET);
+          mSitesPermissions.remove(perm);
+
+          View view = findViewById(R.id.sites_list);
+          if (view != null) {
+            ListView listView = (ListView) view;
+            ListAdapter listAdapter = listView.getAdapter();
+            if (listAdapter != null) {
+              SettingsAdapter settingsAdapter = (SettingsAdapter) listAdapter;
+              settingsAdapter.notifyDataSetChanged();
+            }
+          }
+        }
+      };
+      button.setTag(perms);
+      button.setOnClickListener(listener);
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+      View row = convertView;
+      if (row == null) { // no cached view, we create one
+        LayoutInflater inflater = (LayoutInflater) getSystemService(
+            Context.LAYOUT_INFLATER_SERVICE);
+        row = inflater.inflate(R.layout.gears_dialog_settings_row, null);
+      }
+
+      OriginPermissions perms = (OriginPermissions) mItems.get(position);
+
+      View nameView = row.findViewById(R.id.origin_name);
+      if (nameView != null) {
+        TextView originName = (TextView) nameView;
+        originName.setText(perms.getOrigin());
+      }
+
+      View removeButtonView = row.findViewById(R.id.origin_remove);
+      if (removeButtonView != null) {
+        Button removeButton = (Button) removeButtonView;
+        setRemoveButton(removeButton, perms);
+      }
+
+      for (int i = 0; i < mPermissions.size(); i++) {
+        PermissionType type = mPermissions.get(i);
+        int rowRsc = type.getRowRsc();
+        int allowedButtonRsc = type.getAllowedButtonRsc();
+        int deniedButtonRsc = type.getDeniedButtonRsc();
+
+        View rowView = row.findViewById(rowRsc);
+        if (rowView != null) {
+          int perm = perms.getPermission(type);
+          if (perm != PermissionType.PERMISSION_NOT_SET) {
+            createAndSetButtonListener(row.findViewById(allowedButtonRsc),
+                perms, type, PermissionController.ALLOWED_BUTTON);
+            createAndSetButtonListener(row.findViewById(deniedButtonRsc),
+                perms, type, PermissionController.DENIED_BUTTON);
+            rowView.setVisibility(View.VISIBLE);
+          } else {
+            rowView.setVisibility(View.GONE);
+          }
+        }
+      }
+
+      return row;
+    }
+  }
+
+  /**
+   * Utility method used in debug mode to print the list of
+   * permissions (original values and current values).
+   */
+  public void printPermissions() {
+    Log.v(TAG, "Original Permissions: ");
+    for (int i = 0; i < mOriginalPermissions.size(); i++) {
+      OriginPermissions p = mOriginalPermissions.get(i);
+      p.print();
+    }
+    Log.v(TAG, "Current Permissions: ");
+    for (int i = 0; i < mSitesPermissions.size(); i++) {
+      OriginPermissions p = mSitesPermissions.get(i);
+      p.print();
+    }
+  }
+
+  /**
+   * Utility method used by the settings dialog, signaling
+   * the user the settings have been modified.
+   * We reflect this by enabling the Allow button (disabled
+   * by default).
+   */
+  public void signalChanges() {
+    View view = findViewById(R.id.button_allow);
+    if (view != null) {
+      Button button = (Button) view;
+      button.setEnabled(true);
+    }
+    mChanges = true;
+  }
+
+  /**
+   * Computes the difference between the original permissions and the
+   * current ones. Returns a json-formatted string.
+   * It is used by the Settings dialog.
+   */
+  public String computeDiff(boolean modif) {
+    String ret = null;
+    try {
+      JSONObject results = new JSONObject();
+      JSONArray permissions = new JSONArray();
+
+      for (int i = 0; modif && i < mOriginalPermissions.size(); i++) {
+        OriginPermissions original = mOriginalPermissions.get(i);
+        OriginPermissions current = mCurrentPermissions.get(i);
+        JSONObject permission = new JSONObject();
+        boolean modifications = false;
+
+        for (int j = 0; j < mPermissions.size(); j++) {
+          PermissionType type = mPermissions.get(j);
+
+          if (current.getPermission(type) != original.getPermission(type)) {
+            JSONObject state = new JSONObject();
+            state.put("permissionState", current.getPermission(type));
+            permission.put(type.getName(), state);
+            modifications = true;
+          }
+        }
+
+        if (modifications) {
+          permission.put("name", current.getOrigin());
+          permissions.put(permission);
+        }
+      }
+      results.put("modifiedOrigins", permissions);
+      ret = results.toString();
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON exception ", e);
+    }
+    return ret;
+  }
+
+  public String closeDialog(int closingType) {
+    String ret = null;
+    switch (closingType) {
+      case ALWAYS_DENY:
+        ret = "{\"allow\": false }";
+        break;
+      case ALLOW:
+        ret = computeDiff(true);
+        break;
+      case DENY:
+        ret = computeDiff(false);
+        break;
+    }
+
+    if (mDebug) {
+      printPermissions();
+    }
+
+    return ret;
+  }
+
+}
diff --git a/src/com/android/browser/GearsShortcutDialog.java b/src/com/android/browser/GearsShortcutDialog.java
new file mode 100644
index 0000000..deede12
--- /dev/null
+++ b/src/com/android/browser/GearsShortcutDialog.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.browser;
+
+import android.app.Activity;
+import android.os.Handler;
+import android.util.Log;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Gears Shortcut dialog
+ */
+class GearsShortcutDialog extends GearsBaseDialog {
+
+  private static final String TAG = "GearsPermissionsDialog";
+
+  private final String ICON_16 = "icon16x16";
+  private final String ICON_32 = "icon32x32";
+  private final String ICON_48 = "icon48x48";
+  private final String ICON_128 = "icon128x128";
+
+  public GearsShortcutDialog(Activity activity,
+                             Handler handler,
+                             String arguments) {
+    super (activity, handler, arguments);
+  }
+
+  public void setup() {
+    inflate(R.layout.gears_dialog_permission, R.id.panel_content);
+    setupButtons(R.string.shortcut_button_alwaysdeny,
+                 R.string.shortcut_button_allow,
+                 R.string.shortcut_button_deny);
+
+    View contentBorder = findViewById(R.id.content_border);
+    if (contentBorder != null) {
+      contentBorder.setBackgroundResource(R.color.shortcut_border);
+    }
+    View contentBackground = findViewById(R.id.content_background);
+    if (contentBackground != null) {
+      contentBackground.setBackgroundResource(R.color.shortcut_background);
+    }
+
+    try {
+      JSONObject json = new JSONObject(mDialogArguments);
+
+      String iconUrl = pickIconToRender(json);
+      if (iconUrl != null) {
+        downloadIcon(iconUrl);
+      }
+
+      setupDialog();
+
+      setLabel(json, "name", R.id.origin_title);
+      setLabel(json, "link", R.id.origin_subtitle);
+      setLabel(json, "description", R.id.origin_message);
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON exception", e);
+    }
+  }
+
+  public void setupDialog(TextView message, ImageView icon) {
+    message.setText(R.string.shortcut_message);
+    icon.setImageResource(R.drawable.gears_icon_48x48);
+  }
+
+  /**
+   * Utility method to validate an icon url. Used in the
+   * shortcut dialog.
+   */
+  boolean validIcon(JSONObject json, String name) {
+    try {
+      if (json.has(name)) {
+        String str = json.getString(name);
+        if (str.length() > 0) {
+          return true;
+        }
+      }
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON exception", e);
+    }
+    return false;
+  }
+
+
+  /**
+   * Utility method to pick the best indicated icon
+   * from the dialogs' arguments. Used in the
+   * shortcut dialog.
+   */
+  String pickIconToRender(JSONObject json) {
+    try {
+      if (validIcon(json, ICON_48)) { // ideal size
+        mChoosenIconSize = 48;
+        return json.getString(ICON_48);
+      } else if (validIcon(json, ICON_32)) {
+        mChoosenIconSize = 32;
+        return json.getString(ICON_32);
+      } else if (validIcon(json, ICON_128)) {
+        mChoosenIconSize = 128;
+        return json.getString(ICON_128);
+      } else if (validIcon(json, ICON_16)) {
+        mChoosenIconSize = 16;
+        return json.getString(ICON_16);
+      }
+    } catch (JSONException e) {
+      Log.e(TAG, "JSON exception", e);
+    }
+    mChoosenIconSize = 0;
+    return null;
+  }
+
+  public String closeDialog(int closingType) {
+    String ret = null;
+    switch (closingType) {
+      case ALWAYS_DENY:
+        ret = "{\"allow\": false, \"permanently\": true }";
+        break;
+      case ALLOW:
+        ret = "{\"allow\": true, \"locations\": 0 }";
+        break;
+      case DENY:
+        ret = null;
+        break;
+    }
+    return ret;
+  }
+
+}
diff --git a/src/com/android/browser/ImageAdapter.java b/src/com/android/browser/ImageAdapter.java
index e6eaa75..b4c1209 100644
--- a/src/com/android/browser/ImageAdapter.java
+++ b/src/com/android/browser/ImageAdapter.java
@@ -17,18 +17,20 @@
 package com.android.browser;
 
 import android.app.AlertDialog;
+import android.content.Context;
 import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.Resources;
 import android.database.DataSetObserver;
 import android.graphics.Color;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.LayoutInflater;
-import android.content.Context;
-import android.content.res.Configuration;
-import android.content.res.Resources;
 import android.webkit.WebView;
-import android.widget.*;
+import android.widget.ImageView;
+import android.widget.ListAdapter;
+import android.widget.TextView;
 
 import java.util.ArrayList;
 
@@ -38,31 +40,25 @@
 public class ImageAdapter implements ListAdapter {
     
     ArrayList<TabControl.Tab> mItems;  // Items shown in the grid
-    ArrayList<DataSetObserver> mDataObservers; // Data change listeners
-    Context mContext;  // Context to use to inflate views
-    boolean mMaxedOut;
-    boolean mLandScape;
-    ImageGrid mImageGrid;
-    boolean mIsLive;
+    private ArrayList<DataSetObserver> mDataObservers; // Data change listeners
+    private Context mContext;  // Context to use to inflate views
+    private boolean mMaxedOut;
+    private ImageGrid mImageGrid;
+    private boolean mIsLive;
+    private int mTabHeight;
 
-    ImageAdapter(Context context, ImageGrid grid,
-            ArrayList<TabControl.Tab> items, boolean live) {
+    ImageAdapter(Context context, ImageGrid grid, boolean live) {
         mContext = context;
         mIsLive = live;
-        if (items == null) {
-            mItems = new ArrayList<TabControl.Tab>();
-        } else {
-            mItems = items;
-            if (items.size() == TabControl.MAX_TABS) {
-                mMaxedOut = true;
-            }
-        }
+        mItems = new ArrayList<TabControl.Tab>();
         mImageGrid = grid;
         mDataObservers = new ArrayList<DataSetObserver>();
-        mLandScape = context.getResources().getConfiguration().orientation ==
-                Configuration.ORIENTATION_LANDSCAPE;
     }
-    
+
+    void heightChanged(int newHeight) {
+        mTabHeight = newHeight;
+    }
+
     /**
      *  Whether the adapter is at its limit, determined by TabControl.MAX_TABS
      *
@@ -197,9 +193,9 @@
             TabControl.Tab t = mItems.get(position);
             img.setTab(t);
             tv.setText(t.getTitle());
-            // Do not put the 'X' for a single tab or if the tab picker isn't
-            // "live" (meaning the user cannot click on a tab)
-            if (mItems.size() == 1 || !mIsLive) {
+            // Do not put the 'X' if the tab picker isn't "live" (meaning the
+            // user cannot click on a tab)
+            if (!mIsLive) {
                 close.setVisibility(View.GONE);
             } else {
                 close.setVisibility(View.VISIBLE);
@@ -218,10 +214,9 @@
             tv.setText(R.string.new_window);
             close.setVisibility(View.GONE);
         }
-        if (mLandScape) {
-            ViewGroup.LayoutParams lp = img.getLayoutParams();
-            lp.width = 225;
-            lp.height = 120;
+        ViewGroup.LayoutParams lp = img.getLayoutParams();
+        if (lp.height != mTabHeight) {
+            lp.height = mTabHeight;
             img.requestLayout();
         }
         return v;
@@ -245,7 +240,7 @@
                 };
         new AlertDialog.Builder(mContext)
                 .setTitle(R.string.close)
-                .setIcon(R.drawable.ssl_icon)
+                .setIcon(android.R.drawable.ic_dialog_alert)
                 .setMessage(R.string.close_window)
                 .setPositiveButton(R.string.ok, confirm)
                 .setNegativeButton(R.string.cancel, null)
@@ -257,7 +252,6 @@
      */
     public void registerDataSetObserver(DataSetObserver observer) {
         mDataObservers.add(observer);
-
     }
 
     /* (non-Javadoc)
@@ -272,9 +266,8 @@
      */
     public void unregisterDataSetObserver(DataSetObserver observer) {
         mDataObservers.remove(observer);
-
     }
-    
+
     /**
      * Notify all the observers that a change has happened.
      */
diff --git a/src/com/android/browser/ImageGrid.java b/src/com/android/browser/ImageGrid.java
index e0a5c89..9eccb16 100644
--- a/src/com/android/browser/ImageGrid.java
+++ b/src/com/android/browser/ImageGrid.java
@@ -39,6 +39,7 @@
     private Listener     mListener;
     private ImageAdapter mAdapter;
     private boolean      mIsLive;
+    private static final int SPACING = 10;
     public static final int CANCEL  = -99;
     public static final int NEW_TAB = -1;
 
@@ -58,23 +59,18 @@
             setOnItemClickListener(this);
             setOnCreateContextMenuListener(this);
         }
-        if (Config.DEBUG && l == null) {
-            throw new AssertionError();
-        }
         mListener = l;
 
-        mAdapter = new ImageAdapter(context, this, null, live);
+        mAdapter = new ImageAdapter(context, this, live);
         setAdapter(mAdapter);
 
-        // android.R.color.window_background seems to return transparent?
-//        setBackgroundColor(android.R.color.window_background);
         setBackgroundColor(0xFF000000);
 
-        setPadding(0, 10, 0, 10);
-        setVerticalSpacing(10);
-        setHorizontalSpacing(10);
+        setVerticalSpacing(SPACING);
+        setHorizontalSpacing(SPACING);
         setNumColumns(2);
         setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
+        setSelector(android.R.drawable.gallery_thumb);
     }
 
     @Override
@@ -189,11 +185,6 @@
                 position--;
             }
             menu.setHeaderTitle(mAdapter.mItems.get(position).getTitle());
-
-            // If we only have one active tab left, don't add the remove option
-            if (mAdapter.mItems.size() <= 1) {
-                menu.findItem(R.id.remove_tab_menu_id).setVisible(false);
-            }
         }
     }
 
@@ -212,10 +203,13 @@
     
     @Override
     protected void onSizeChanged(int w, int h, int oldw, int oldh) {
-        // Called when our orientation changes.  Replace the adapter with one
-        // that has the appropriate dimensions.
-        mAdapter = new ImageAdapter(mContext, this, mAdapter.mItems, mIsLive);
-        setAdapter(mAdapter);
+        // Called when our orientation changes. Tell the adapter about the new
+        // size. Compute the individual tab height by taking the grid height
+        // and subtracting the SPACING. Then subtract the list padding twice
+        // (once for each tab on screen) and divide the remaining height by 2.
+        int tabHeight = (h - SPACING
+                - 2 * (getListPaddingTop() + getListPaddingBottom())) / 2;
+        mAdapter.heightChanged(tabHeight);
         super.onSizeChanged(w, h, oldw, oldh);
     }
 
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
index 66adf3c..01ef3a8 100644
--- a/src/com/android/browser/TabControl.java
+++ b/src/com/android/browser/TabControl.java
@@ -35,6 +35,7 @@
 import android.widget.FrameLayout;
 import android.widget.ImageButton;
 
+import java.io.File;
 import java.util.ArrayList;
 import java.util.Vector;
 
@@ -169,9 +170,12 @@
         // children.
         private Vector<Tab> mChildTabs;
 
+        private Boolean mCloseOnExit;
+
         // Construct a new tab
-        private Tab(WebView w) {
+        private Tab(WebView w, boolean closeOnExit) {
             mMainView = w;
+            mCloseOnExit = closeOnExit;
         }
 
         /**
@@ -236,6 +240,17 @@
 
         private void setParentTab(Tab parent) {
             mParentTab = parent;
+            // This tab may have been freed due to low memory. If that is the
+            // case, the parent tab index is already saved. If we are changing
+            // that index (most likely due to removing the parent tab) we must
+            // update the parent tab index in the saved Bundle.
+            if (mSavedState != null) {
+                if (parent == null) {
+                    mSavedState.remove(PARENTTAB);
+                } else {
+                    mSavedState.putInt(PARENTTAB, getTabIndex(parent));
+                }
+            }
         }
         
         /**
@@ -273,8 +288,20 @@
         public Tab getParentTab() {
             return mParentTab;
         }
+
+        /**
+         * Return whether this tab should be closed when it is backing out of
+         * the first page.
+         * @return TRUE if this tab should be closed when exit.
+         */
+        public boolean closeOnExit() {
+            return mCloseOnExit;
+        }
     };
 
+    // Directory to store thumbnails for each WebView.
+    private final File mThumbnailDir;
+
     /**
      * Construct a new TabControl object that interfaces with the given
      * BrowserActivity instance.
@@ -286,6 +313,15 @@
         mInflateService =
                 ((LayoutInflater) activity.getSystemService(
                         Context.LAYOUT_INFLATER_SERVICE));
+        mThumbnailDir = activity.getDir("thumbnails", 0);
+    }
+
+    File getThumbnailDir() {
+        return mThumbnailDir;
+    }
+
+    BrowserActivity getBrowserActivity() {
+        return mActivity;
     }
 
     /**
@@ -368,7 +404,7 @@
      * @return The newly createTab or null if we have reached the maximum
      *         number of open tabs.
      */
-    Tab createNewTab() {
+    Tab createNewTab(boolean closeOnExit) {
         int size = mTabs.size();
         // Return false if we have maxed out on tabs
         if (MAX_TABS == size) {
@@ -382,7 +418,7 @@
         final BrowserSettings s = BrowserSettings.getInstance();
         s.addObserver(w.getSettings()).update(s, null);
         // Create a new tab and add it to the tab list
-        Tab t = new Tab(w);
+        Tab t = new Tab(w, closeOnExit);
         mTabs.add(t);
         return t;
     }
@@ -418,6 +454,26 @@
         
         // Remove it from our list of tabs.
         mTabs.remove(t);
+
+        // The tab indices have shifted, update all the saved state so we point
+        // to the correct index.
+        for (Tab tab : mTabs) {
+            if (tab.mChildTabs != null) {
+                for (Tab child : tab.mChildTabs) {
+                    child.setParentTab(tab);
+                }
+            }
+        }
+
+
+        // This tab may have been pushed in to the background and then closed.
+        // If the saved state contains a picture file, delete the file.
+        if (t.mSavedState != null) {
+            if (t.mSavedState.containsKey("picture")) {
+                new File(t.mSavedState.getString("picture")).delete();
+            }
+        }
+
         // Remove it from the queue of viewed tabs.
         mTabQueue.remove(t);
         mCurrentTab = -1;
@@ -473,6 +529,8 @@
     private static final String CURRTAB = "currentTab";
     private static final String CURRURL = "currentUrl";
     private static final String CURRTITLE = "currentTitle";
+    private static final String CLOSEONEXIT = "closeonexit";
+    private static final String PARENTTAB = "parentTab";
 
     /**
      * Save the state of all the Tabs.
@@ -506,7 +564,7 @@
             final int currentTab = inState.getInt(CURRTAB, -1);
             for (int i = 0; i < numTabs; i++) {
                 if (i == currentTab) {
-                    Tab t = createNewTab();
+                    Tab t = createNewTab(false);
                     // Me must set the current tab before restoring the state
                     // so that all the client classes are set.
                     setCurrentTab(t);
@@ -518,7 +576,7 @@
                 } else {
                     // Create a new tab and don't restore the state yet, add it
                     // to the tab list
-                    Tab t = new Tab(null);
+                    Tab t = new Tab(null, false);
                     t.mSavedState = inState.getBundle(WEBVIEW + i);
                     if (t.mSavedState != null) {
                         t.mUrl = t.mSavedState.getString(CURRURL);
@@ -528,6 +586,21 @@
                     mTabQueue.add(t);
                 }
             }
+            // Rebuild the tree of tabs. Do this after all tabs have been
+            // created/restored so that the parent tab exists.
+            for (int i = 0; i < numTabs; i++) {
+                final Bundle b = inState.getBundle(WEBVIEW + i);
+                final Tab t = getTab(i);
+                if (b != null && t != null) {
+                    final int parentIndex = b.getInt(PARENTTAB, -1);
+                    if (parentIndex != -1) {
+                        final Tab parent = getTab(parentIndex);
+                        if (parent != null) {
+                            parent.addChildTab(t);
+                        }
+                    }
+                }
+            }
         }
         return true;
     }
@@ -801,9 +874,16 @@
             }
             final Bundle b = new Bundle();
             final WebBackForwardList list = w.saveState(b);
+            if (list != null) {
+                final File f = new File(mThumbnailDir, w.hashCode()
+                        + "_pic.save");
+                if (w.savePicture(b, f)) {
+                    b.putString("picture", f.getPath());
+                }
+            }
 
             // Store some extra info for displaying the tab in the picker.
-            final WebHistoryItem item = 
+            final WebHistoryItem item =
                     list != null ? list.getCurrentItem() : null;
             populatePickerData(t, item);
             if (t.mUrl != null) {
@@ -812,6 +892,12 @@
             if (t.mTitle != null) {
                 b.putString(CURRTITLE, t.mTitle);
             }
+            b.putBoolean(CLOSEONEXIT, t.mCloseOnExit);
+
+            // Remember the parent tab so the relationship can be restored.
+            if (t.mParentTab != null) {
+                b.putInt(PARENTTAB, getTabIndex(t.mParentTab));
+            }
 
             // Remember the saved state.
             t.mSavedState = b;
@@ -832,9 +918,15 @@
         if (list == null) {
             return false;
         }
+        if (b.containsKey("picture")) {
+            final File f = new File(b.getString("picture"));
+            w.restorePicture(b, f);
+            f.delete();
+        }
         t.mSavedState = null;
         t.mUrl = null;
         t.mTitle = null;
+        t.mCloseOnExit = b.getBoolean(CLOSEONEXIT);
         return true;
     }
 }