Change SWE app properties back to stock Android

- Changed project package name from com.android.swe.browser
back to com.android.browser along with code references to
old package name.
- Changes to AndroidManifest making it conform closer to stock
browser manifest.
- Changed app and apk name back to Browser.

Change-Id: I778ee1d1197bd50bd4a4850eef6d1d7f4ef0ad0b
diff --git a/src/com/android/browser/AccountsChangedReceiver.java b/src/com/android/browser/AccountsChangedReceiver.java
new file mode 100644
index 0000000..a4d10d7
--- /dev/null
+++ b/src/com/android/browser/AccountsChangedReceiver.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+import android.text.TextUtils;
+
+public class AccountsChangedReceiver extends BroadcastReceiver {
+
+    private static final String[] PROJECTION = new String[] {
+        Accounts.ACCOUNT_NAME,
+        Accounts.ACCOUNT_TYPE,
+    };
+    private static final String SELECTION = Accounts.ACCOUNT_NAME + " IS NOT NULL";
+    private static final String DELETE_SELECTION = Accounts.ACCOUNT_NAME + "=? AND "
+            + Accounts.ACCOUNT_TYPE + "=?";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        new DeleteRemovedAccounts(context).start();
+    }
+
+    static class DeleteRemovedAccounts extends Thread {
+        Context mContext;
+        public DeleteRemovedAccounts(Context context) {
+            mContext = context.getApplicationContext();
+        }
+
+        @Override
+        public void run() {
+            Account[] accounts = AccountManager.get(mContext).getAccounts();
+            ContentResolver cr = mContext.getContentResolver();
+            Cursor c = cr.query(Accounts.CONTENT_URI, PROJECTION,
+                    SELECTION, null, null);
+            while (c.moveToNext()) {
+                String name = c.getString(0);
+                String type = c.getString(1);
+                if (!contains(accounts, name, type)) {
+                    delete(cr, name, type);
+                }
+            }
+            cr.update(Accounts.CONTENT_URI, null, null, null);
+            c.close();
+        }
+
+        void delete(ContentResolver cr, String name, String type) {
+            // Pretend to be a sync adapter to delete the data and not mark
+            // it for deletion. Without this, the bookmarks will be marked to
+            // be deleted, which will propagate to the server if the account
+            // is added back.
+            Uri uri = Bookmarks.CONTENT_URI.buildUpon()
+                    .appendQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, "true")
+                    .build();
+            cr.delete(uri, DELETE_SELECTION, new String[] { name, type });
+        }
+
+        boolean contains(Account[] accounts, String name, String type) {
+            for (Account a : accounts) {
+                if (TextUtils.equals(a.name, name)
+                        && TextUtils.equals(a.type, type)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+}
diff --git a/src/com/android/browser/ActivityController.java b/src/com/android/browser/ActivityController.java
new file mode 100644
index 0000000..ac248b8
--- /dev/null
+++ b/src/com/android/browser/ActivityController.java
@@ -0,0 +1,74 @@
+package com.android.browser;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+
+public interface ActivityController {
+
+    void start(Intent intent);
+
+    void onSaveInstanceState(Bundle outState);
+
+    void handleNewIntent(Intent intent);
+
+    void onResume();
+
+    boolean onMenuOpened(int featureId, Menu menu);
+
+    void onOptionsMenuClosed(Menu menu);
+
+    void onContextMenuClosed(Menu menu);
+
+    void onPause();
+
+    void onDestroy();
+
+    void onConfgurationChanged(Configuration newConfig);
+
+    void onLowMemory();
+
+    boolean onCreateOptionsMenu(Menu menu);
+
+    boolean onPrepareOptionsMenu(Menu menu);
+
+    boolean onOptionsItemSelected(MenuItem item);
+
+    void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo);
+
+    boolean onContextItemSelected(MenuItem item);
+
+    boolean onKeyDown(int keyCode, KeyEvent event);
+
+    boolean onKeyLongPress(int keyCode, KeyEvent event);
+
+    boolean onKeyUp(int keyCode, KeyEvent event);
+
+    void onActionModeStarted(ActionMode mode);
+
+    void onActionModeFinished(ActionMode mode);
+
+    void onActivityResult(int requestCode, int resultCode, Intent intent);
+
+    boolean onSearchRequested();
+
+    boolean dispatchKeyEvent(KeyEvent event);
+
+    boolean dispatchKeyShortcutEvent(KeyEvent event);
+
+    boolean dispatchTouchEvent(MotionEvent ev);
+
+    boolean dispatchTrackballEvent(MotionEvent ev);
+
+    boolean dispatchGenericMotionEvent(MotionEvent ev);
+
+}
diff --git a/src/com/android/browser/AddBookmarkFolder.java b/src/com/android/browser/AddBookmarkFolder.java
new file mode 100644
index 0000000..4a9c13c
--- /dev/null
+++ b/src/com/android/browser/AddBookmarkFolder.java
@@ -0,0 +1,951 @@
+/*
+ * Copyright (C) 2006 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.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+
+import android.text.TextUtils;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.addbookmark.FolderSpinner;
+import com.android.browser.addbookmark.FolderSpinnerAdapter;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.provider.BrowserProvider2;
+import com.android.browser.reflect.ReflectHelper;
+
+public class AddBookmarkFolder extends Activity implements View.OnClickListener,
+        TextView.OnEditorActionListener, AdapterView.OnItemClickListener,
+        LoaderManager.LoaderCallbacks<Cursor>, BreadCrumbView.Controller,
+        FolderSpinner.OnSetSelectionListener, OnItemSelectedListener {
+
+    public static final long DEFAULT_FOLDER_ID = -1;
+
+    // Place on an edited bookmark to remove the saved thumbnail
+    public static final String CHECK_FOR_DUPE = "check_for_dupe";
+
+    public static final String BOOKMARK_CURRENT_ID = "bookmark_current_id";
+
+    /* package */static final String EXTRA_EDIT_BOOKMARK = "bookmark";
+
+    /* package */static final String EXTRA_IS_FOLDER = "is_folder";
+
+    private static final int MAX_CRUMBS_SHOWN = 1;
+
+    private long mOriginalFolder = -1;
+
+    private boolean mIsFolderChanged = false;
+
+    private boolean mIsOtherFolderSelected = false;
+
+    private boolean mIsRecentFolder = false;
+
+    // IDs for the CursorLoaders that are used.
+    private static final int LOADER_ID_ACCOUNTS = 0;
+
+    private static final int LOADER_ID_FOLDER_CONTENTS = 1;
+
+    private static final int LOADER_ID_EDIT_INFO = 2;
+
+    private EditText mTitle;
+
+    private EditText mAddress;
+
+    private TextView mButton;
+
+    private View mCancelButton;
+
+    private Bundle mMap;
+
+    private FolderSpinner mFolder;
+
+    private View mDefaultView;
+
+    private View mFolderSelector;
+
+    private EditText mFolderNamer;
+
+    private View mFolderCancel;
+
+    private boolean mIsFolderNamerShowing;
+
+    private View mFolderNamerHolder;
+
+    private View mAddNewFolder;
+
+    private View mAddSeparator;
+
+    private long mCurrentFolder;
+
+    private FolderAdapter mAdapter;
+
+    private BreadCrumbView mCrumbs;
+
+    private TextView mFakeTitle;
+
+    private View mCrumbHolder;
+
+    private AddBookmarkPage.CustomListView mListView;
+
+    private long mRootFolder;
+
+    private TextView mTopLevelLabel;
+
+    private Drawable mHeaderIcon;
+
+    private View mRemoveLink;
+
+    private View mFakeTitleHolder;
+
+    private FolderSpinnerAdapter mFolderAdapter;
+
+    private Spinner mAccountSpinner;
+
+    private ArrayAdapter<BookmarkAccount> mAccountAdapter;
+
+
+    private static class Folder {
+        String mName;
+
+        long mId;
+
+        Folder(String name, long id) {
+            mName = name;
+            mId = id;
+        }
+    }
+
+    private InputMethodManager getInputMethodManager() {
+        return (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+    }
+
+    private Uri getUriForFolder(long folder) {
+        BookmarkAccount account = (BookmarkAccount) mAccountSpinner.getSelectedItem();
+        if (folder == mRootFolder && account != null) {
+            return BookmarksLoader.addAccount(BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
+                    account.mAccountType, account.mAccountName);
+        }
+        return BrowserContract.Bookmarks.buildFolderUri(folder);
+    }
+
+    public static long getIdFromData(Object data) {
+        if (data == null) {
+            return BrowserProvider2.FIXED_ID_ROOT;
+        } else {
+            Folder folder = (Folder) data;
+            return folder.mId;
+        }
+    }
+
+    @Override
+    public void onTop(BreadCrumbView view, int level, Object data) {
+        if (null == data) {
+            return;
+        }
+        Folder folderData = (Folder) data;
+        long folder = folderData.mId;
+        LoaderManager manager = getLoaderManager();
+        CursorLoader loader = (CursorLoader) ((Loader<?>) manager
+                .getLoader(LOADER_ID_FOLDER_CONTENTS));
+        loader.setUri(getUriForFolder(folder));
+        loader.forceLoad();
+        if (mIsFolderNamerShowing) {
+            completeOrCancelFolderNaming(true);
+        }
+        setShowBookmarkIcon(level == 1);
+    }
+
+    /**
+     * Show or hide the icon for bookmarks next to "Bookmarks" in the crumb
+     * view.
+     *
+     * @param show True if the icon should visible, false otherwise.
+     */
+    private void setShowBookmarkIcon(boolean show) {
+        Drawable drawable = show ? mHeaderIcon : null;
+        mTopLevelLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+    }
+
+    @Override
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (v == mFolderNamer) {
+            if (v.getText().length() > 0) {
+                if (actionId == EditorInfo.IME_NULL) {
+                    // Only want to do this once.
+                    if (event.getAction() == KeyEvent.ACTION_UP) {
+                        completeOrCancelFolderNaming(false);
+                    }
+                }
+            }
+            // Steal the key press; otherwise a newline will be added
+            return true;
+        }
+        return false;
+    }
+
+    private void switchToDefaultView(boolean changedFolder) {
+        mFolderSelector.setVisibility(View.GONE);
+        mDefaultView.setVisibility(View.VISIBLE);
+        mCrumbHolder.setVisibility(View.GONE);
+        mFakeTitleHolder.setVisibility(View.VISIBLE);
+        if (changedFolder) {
+            Object data = mCrumbs.getTopData();
+            if (data != null) {
+                Folder folder = (Folder) data;
+                mCurrentFolder = folder.mId;
+                if (mCurrentFolder == mRootFolder) {
+                    // The Spinner changed to show "Other folder ..." Change
+                    // it back to "Bookmarks", which is position 0 if we are
+                    // editing a folder, 1 otherwise.
+                    mFolder.setSelectionIgnoringSelectionChange(0);
+                } else {
+                    mFolderAdapter.setOtherFolderDisplayText(folder.mName);
+                }
+            }
+        } else {
+            if (mCurrentFolder == mRootFolder) {
+                mFolder.setSelectionIgnoringSelectionChange(0);
+            } else {
+                Object data = mCrumbs.getTopData();
+                if (data != null && ((Folder) data).mId == mCurrentFolder) {
+                    // We are showing the correct folder hierarchy. The
+                    // folder selector will say "Other folder..." Change it
+                    // to say the name of the folder once again.
+                    mFolderAdapter.setOtherFolderDisplayText(((Folder) data).mName);
+                } else {
+                    // We are not showing the correct folder hierarchy.
+                    // Clear the Crumbs and find the proper folder
+                    setupTopCrumb();
+                    LoaderManager manager = getLoaderManager();
+                    manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v == mButton) {
+            if (mFolderSelector.getVisibility() == View.VISIBLE) {
+                // We are showing the folder selector.
+                if (mIsFolderNamerShowing) {
+                    completeOrCancelFolderNaming(false);
+                } else {
+                    switchToDefaultView(true);
+                }
+            } else {
+                if (save()) {
+                    finish();
+                }
+            }
+        } else if (v == mCancelButton) {
+            if (mIsFolderNamerShowing) {
+                completeOrCancelFolderNaming(true);
+            } else if (mFolderSelector.getVisibility() == View.VISIBLE) {
+                switchToDefaultView(false);
+            } else {
+                finish();
+            }
+        } else if (v == mFolderCancel) {
+            completeOrCancelFolderNaming(true);
+        }
+    }
+
+    private void displayToastForExistingFolder() {
+        Toast.makeText(getApplicationContext(), R.string.duplicated_folder_warning,
+                Toast.LENGTH_LONG).show();
+    }
+
+    @Override
+    public void onSetSelection(long id) {
+        int intId = (int) id;
+        mIsFolderChanged = true;
+        mIsOtherFolderSelected = false;
+        mIsRecentFolder = false;
+        switch (intId) {
+            case FolderSpinnerAdapter.ROOT_FOLDER:
+                mCurrentFolder = mRootFolder;
+                mOriginalFolder = mCurrentFolder;
+                break;
+            case FolderSpinnerAdapter.HOME_SCREEN:
+
+                break;
+            case FolderSpinnerAdapter.OTHER_FOLDER:
+                mIsOtherFolderSelected = true;
+                switchToFolderSelector();
+                break;
+            case FolderSpinnerAdapter.RECENT_FOLDER:
+                mCurrentFolder = mFolderAdapter.recentFolderId();
+                mOriginalFolder = mCurrentFolder;
+                mIsRecentFolder = true;
+                // In case the user decides to select OTHER_FOLDER
+                // and choose a different one, so that we will start from
+                // the correct place.
+                LoaderManager manager = getLoaderManager();
+                manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Finish naming a folder, and close the IME
+     *
+     * @param cancel If true, the new folder is not created. If false, the new
+     *            folder is created and the user is taken inside it.
+     */
+    private void completeOrCancelFolderNaming(boolean cancel) {
+        if (!cancel && !TextUtils.isEmpty(mFolderNamer.getText())) {
+            String name = mFolderNamer.getText().toString();
+            long id = addFolderToCurrent(mFolderNamer.getText().toString());
+            descendInto(name, id);
+        }
+        setShowFolderNamer(false);
+        getInputMethodManager().hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+    }
+
+    private long addFolderToCurrent(String name) {
+        // Add the folder to the database
+        ContentValues values = new ContentValues();
+        values.put(BrowserContract.Bookmarks.TITLE, name);
+        values.put(BrowserContract.Bookmarks.IS_FOLDER, 1);
+        long currentFolder;
+        Object data = null;
+        if (null != mCrumbs) {
+            data = mCrumbs.getTopData();
+        }
+        if (data != null) {
+            currentFolder = ((Folder) data).mId;
+        } else {
+            currentFolder = mRootFolder;
+        }
+        currentFolder = mCurrentFolder;
+        if (mIsRecentFolder) {
+            values.put(BrowserContract.Bookmarks.PARENT, mCurrentFolder);
+        } else if (!(mIsFolderChanged && mIsOtherFolderSelected) && mOriginalFolder != -1) {
+            values.put(BrowserContract.Bookmarks.PARENT, mOriginalFolder);
+        } else {
+            values.put(BrowserContract.Bookmarks.PARENT, currentFolder);
+        }
+        Uri uri = getContentResolver().insert(BrowserContract.Bookmarks.CONTENT_URI, values);
+        if (uri != null) {
+            return ContentUris.parseId(uri);
+        } else {
+            return -1;
+        }
+    }
+
+    private void switchToFolderSelector() {
+        // Set the list to the top in case it is scrolled.
+        mListView.setSelection(0);
+        mFakeTitleHolder.setVisibility(View.GONE);
+        // mFakeTitle.setVisibility(View.GONE);
+        mDefaultView.setVisibility(View.GONE);
+        mFolderSelector.setVisibility(View.VISIBLE);
+        mCrumbHolder.setVisibility(View.VISIBLE);
+        getInputMethodManager().hideSoftInputFromWindow(mListView.getWindowToken(), 0);
+    }
+
+    private void descendInto(String foldername, long id) {
+        if (id != DEFAULT_FOLDER_ID) {
+            mCrumbs.pushView(foldername, new Folder(foldername, id));
+            mCrumbs.notifyController();
+        } else {
+            Toast.makeText(getApplicationContext(), R.string.duplicated_folder_warning,
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private LoaderCallbacks<EditBookmarkInfo> mEditInfoLoaderCallbacks = new LoaderCallbacks<EditBookmarkInfo>() {
+
+        @Override
+        public void onLoaderReset(Loader<EditBookmarkInfo> loader) {
+            // Don't care
+        }
+
+        @Override
+        public void onLoadFinished(Loader<EditBookmarkInfo> loader, EditBookmarkInfo info) {
+            boolean setAccount = false;
+            // TODO: Detect if lastUsedId is a subfolder of info.id in the
+            // editing folder case. For now, just don't show the last used
+            // folder at all to prevent any chance of the user adding a parent
+            // folder to a child folder
+            if (info.mLastUsedId != -1 && info.mLastUsedId != info.mId) {
+                if (setAccount && info.mLastUsedId != mRootFolder
+                        && TextUtils.equals(info.mLastUsedAccountName, info.mAccountName)
+                        && TextUtils.equals(info.mLastUsedAccountType, info.mAccountType)) {
+                    mFolderAdapter.addRecentFolder(info.mLastUsedId, info.mLastUsedTitle);
+                } else if (!setAccount) {
+                    setAccount = true;
+                    setAccount(info.mLastUsedAccountName, info.mLastUsedAccountType);
+                    if (info.mLastUsedId != mRootFolder) {
+                        mFolderAdapter.addRecentFolder(info.mLastUsedId, info.mLastUsedTitle);
+                    }
+                }
+            }
+            if (!setAccount) {
+                mAccountSpinner.setSelection(0);
+            }
+        }
+
+        @Override
+        public Loader<EditBookmarkInfo> onCreateLoader(int id, Bundle args) {
+            return new EditBookmarkInfoLoader(AddBookmarkFolder.this, mMap);
+        }
+    };
+
+    void setAccount(String accountName, String accountType) {
+        for (int i = 0; i < mAccountAdapter.getCount(); i++) {
+            BookmarkAccount account = mAccountAdapter.getItem(i);
+            if (TextUtils.equals(account.mAccountName, accountName)
+                    && TextUtils.equals(account.mAccountType, accountType)) {
+                mAccountSpinner.setSelection(i);
+                onRootFolderFound(account.rootFolderId);
+                return;
+            }
+        }
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        String[] projection;
+        switch (id) {
+            case LOADER_ID_ACCOUNTS:
+                return new AccountsLoader(this);
+            case LOADER_ID_FOLDER_CONTENTS:
+                projection = new String[] {
+                        BrowserContract.Bookmarks._ID, BrowserContract.Bookmarks.TITLE,
+                        BrowserContract.Bookmarks.IS_FOLDER
+                };
+                String where = BrowserContract.Bookmarks.IS_FOLDER + " != 0" + " AND "
+                        + BrowserContract.Bookmarks._ID + " != ?";
+                String whereArgs[] = new String[] {
+                        Long.toString(mMap.getLong(BrowserContract.Bookmarks._ID))
+                };
+                long currentFolder;
+                Object data = mCrumbs.getTopData();
+                if (data != null) {
+                    currentFolder = ((Folder) data).mId;
+                } else {
+                    currentFolder = mRootFolder;
+                }
+                return new CursorLoader(this, getUriForFolder(currentFolder), projection, where,
+                        whereArgs, BrowserContract.Bookmarks._ID + " ASC");
+            default:
+                throw new AssertionError("Asking for nonexistant loader!");
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        switch (loader.getId()) {
+            case LOADER_ID_ACCOUNTS:
+                mAccountAdapter.clear();
+                while (cursor.moveToNext()) {
+                    mAccountAdapter.add(new BookmarkAccount(this, cursor));
+                }
+                getLoaderManager().destroyLoader(LOADER_ID_ACCOUNTS);
+                getLoaderManager().restartLoader(LOADER_ID_EDIT_INFO, null,
+                        mEditInfoLoaderCallbacks);
+                break;
+            case LOADER_ID_FOLDER_CONTENTS:
+                mAdapter.changeCursor(cursor);
+                break;
+            default:
+                break;
+        }
+    }
+
+    public void onLoaderReset(Loader<Cursor> loader) {
+        switch (loader.getId()) {
+            case LOADER_ID_FOLDER_CONTENTS:
+                mAdapter.changeCursor(null);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Move cursor to the position that has folderToFind as its "_id".
+     *
+     * @param cursor Cursor containing folders in the bookmarks database
+     * @param folderToFind "_id" of the folder to move to.
+     * @param idIndex Index in cursor of "_id"
+     * @throws AssertionError if cursor is empty or there is no row with
+     *             folderToFind as its "_id".
+     */
+    void moveCursorToFolder(Cursor cursor, long folderToFind, int idIndex) throws AssertionError {
+        if (!cursor.moveToFirst()) {
+            throw new AssertionError("No folders in the database!");
+        }
+        long folder;
+        do {
+            folder = cursor.getLong(idIndex);
+        } while (folder != folderToFind && cursor.moveToNext());
+        if (cursor.isAfterLast()) {
+            throw new AssertionError("Folder(id=" + folderToFind
+                    + ") holding this bookmark does not exist!");
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+        TextView tv = (TextView) view.findViewById(android.R.id.text1);
+        // Switch to the folder that was clicked on.
+        descendInto(tv.getText().toString(), id);
+    }
+
+    private void setShowFolderNamer(boolean show) {
+        if (show != mIsFolderNamerShowing) {
+            mIsFolderNamerShowing = show;
+            if (show) {
+                // Set the selection to the folder namer so it will be in
+                // view.
+                mListView.addFooterView(mFolderNamerHolder);
+            } else {
+                mListView.removeFooterView(mFolderNamerHolder);
+            }
+            // Refresh the list.
+            mListView.setAdapter(mAdapter);
+            if (show) {
+                mListView.setSelection(mListView.getCount() - 1);
+            }
+        }
+    }
+
+    /**
+     * Shows a list of names of folders.
+     */
+    private class FolderAdapter extends CursorAdapter {
+        public FolderAdapter(Context context) {
+            super(context, null);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            ((TextView) view.findViewById(android.R.id.text1)).setText(cursor.getString(cursor
+                    .getColumnIndexOrThrow(BrowserContract.Bookmarks.TITLE)));
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            View view = LayoutInflater.from(context).inflate(R.layout.folder_list_item, null);
+            view.setBackgroundDrawable(context.getResources().getDrawable(
+                    android.R.drawable.list_selector_background));
+            return view;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            // Do not show the empty view if the user is creating a new folder.
+            return super.isEmpty() && !mIsFolderNamerShowing;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+        mMap = getIntent().getExtras();
+
+        setContentView(R.layout.browser_add_bookmark);
+
+        Window window = getWindow();
+
+        String title = this.getString(R.string.new_folder);
+        mFakeTitle = (TextView) findViewById(R.id.fake_title);
+        mFakeTitleHolder = findViewById(R.id.title_holder);
+        mFakeTitle.setText(this.getString(R.string.new_folder));
+
+        mTitle = (EditText) findViewById(R.id.title);
+        // add for cmcc test about waring limit of edit text
+        BrowserUtils.maxLengthFilter(AddBookmarkFolder.this, mTitle, BrowserUtils.FILENAME_MAX_LENGTH);
+
+        mTitle.setText(title);
+        mAddress = (EditText) findViewById(R.id.address);
+        mAddress.setVisibility(View.GONE);
+        findViewById(R.id.row_address).setVisibility(View.GONE);
+
+        mButton = (TextView) findViewById(R.id.OK);
+        mButton.setOnClickListener(this);
+
+        mCancelButton = findViewById(R.id.cancel);
+        mCancelButton.setOnClickListener(this);
+
+        mFolder = (FolderSpinner) findViewById(R.id.folder);
+        mFolderAdapter = new FolderSpinnerAdapter(this, false);
+        mFolder.setAdapter(mFolderAdapter);
+        mFolder.setOnSetSelectionListener(this);
+
+        mDefaultView = findViewById(R.id.default_view);
+        mFolderSelector = findViewById(R.id.folder_selector);
+
+        mFolderNamerHolder = getLayoutInflater().inflate(R.layout.new_folder_layout, null);
+        mFolderNamer = (EditText) mFolderNamerHolder.findViewById(R.id.folder_namer);
+        mFolderNamer.setOnEditorActionListener(this);
+        mFolderCancel = mFolderNamerHolder.findViewById(R.id.close);
+        mFolderCancel.setOnClickListener(this);
+
+        mAddNewFolder = findViewById(R.id.add_new_folder);
+        mAddNewFolder.setVisibility(View.GONE);
+        mAddSeparator = findViewById(R.id.add_divider);
+        mAddSeparator.setVisibility(View.GONE);
+
+        mCrumbs = (BreadCrumbView) findViewById(R.id.crumbs);
+        mCrumbs.setUseBackButton(true);
+        mCrumbs.setController(this);
+        mHeaderIcon = getResources().getDrawable(R.drawable.ic_folder_holo_dark);
+        mCrumbHolder = findViewById(R.id.crumb_holder);
+        mCrumbs.setMaxVisible(MAX_CRUMBS_SHOWN);
+
+        mAdapter = new FolderAdapter(this);
+        mListView = (AddBookmarkPage.CustomListView) findViewById(R.id.list);
+        View empty = findViewById(R.id.empty);
+        mListView.setEmptyView(empty);
+        mListView.setAdapter(mAdapter);
+        mListView.setOnItemClickListener(this);
+        mListView.addEditText(mFolderNamer);
+
+        mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this,
+                android.R.layout.simple_spinner_item);
+        mAccountAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+        mAccountSpinner = (Spinner) findViewById(R.id.accounts);
+        mAccountSpinner.setAdapter(mAccountAdapter);
+        mAccountSpinner.setOnItemSelectedListener(this);
+
+        if (!window.getDecorView().isInTouchMode()) {
+            mButton.requestFocus();
+        }
+        // getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+
+        setShowFolderNamer(false);
+        mFolderNamer.setText(R.string.new_folder);
+        mFolderNamer.requestFocus();
+        InputMethodManager imm = getInputMethodManager();
+        Object[] params  = {mListView};
+        Class[] type = new Class[] {View.class};
+        ReflectHelper.invokeMethod(imm, "focusIn", type, params);
+        imm.showSoftInput(mFolderNamer, InputMethodManager.SHOW_IMPLICIT);
+
+        mCurrentFolder = getIntent().getLongExtra(
+                BrowserContract.Bookmarks.PARENT, DEFAULT_FOLDER_ID);
+        mOriginalFolder = mCurrentFolder;
+        if (!(mCurrentFolder == -1 || mCurrentFolder == 1)) {
+            mFolder.setSelectionIgnoringSelectionChange(1);
+            mFolderAdapter.setOtherFolderDisplayText(getNameFromId(mOriginalFolder));
+        }
+
+        getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+    }
+
+    // get folder title from folder id
+    private String getNameFromId(long mCurrentFolder2) {
+        String title = "";
+        Cursor cursor = null;
+        try {
+            cursor = getApplicationContext().getContentResolver().query(
+                    BrowserContract.Bookmarks.CONTENT_URI,
+                    new String[] {
+                        BrowserContract.Bookmarks.TITLE
+                    },
+                    BrowserContract.Bookmarks._ID + " = ? AND "
+                            + BrowserContract.Bookmarks.IS_DELETED + " = ? AND "
+                            + BrowserContract.Bookmarks.IS_FOLDER + " = ? ", new String[] {
+                            String.valueOf(mCurrentFolder2), 0 + "", 1 + ""
+                    }, null);
+            if (cursor != null && cursor.getCount() != 0) {
+                while (cursor.moveToNext()) {
+                    title = cursor.getString(0);
+                }
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+        return title;
+    }
+
+    private void showRemoveButton() {
+        findViewById(R.id.remove_divider).setVisibility(View.VISIBLE);
+        mRemoveLink = findViewById(R.id.remove);
+        mRemoveLink.setVisibility(View.VISIBLE);
+        mRemoveLink.setOnClickListener(this);
+    }
+
+    // Called once we have determined which folder is the root folder
+    private void onRootFolderFound(long root) {
+        mRootFolder = root;
+        mCurrentFolder = mRootFolder;
+        setupTopCrumb();
+        onCurrentFolderFound();
+    }
+
+    private void setupTopCrumb() {
+        mCrumbs.clear();
+        String name = getString(R.string.bookmarks);
+        mTopLevelLabel = (TextView) mCrumbs.pushView(name, false, new Folder(name, mRootFolder));
+        // To better match the other folders.
+        mTopLevelLabel.setCompoundDrawablePadding(6);
+    }
+
+    private void onCurrentFolderFound() {
+        LoaderManager manager = getLoaderManager();
+        if (mCurrentFolder != mRootFolder) {
+            // Since we're not in the root folder, change the selection to other
+            // folder now. The text will get changed once we select the correct
+            // folder.
+            mFolder.setSelectionIgnoringSelectionChange(1);
+        } else {
+            setShowBookmarkIcon(true);
+        }
+        // Find the contents of the current folder
+        manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+    }
+
+    /**
+     * Parse the data entered in the dialog and post a message to update the
+     * bookmarks database.
+     */
+    private boolean save() {
+        String title = mTitle.getText().toString().trim();
+
+        boolean emptyTitle = title.length() == 0;
+        Resources r = getResources();
+        if (emptyTitle) {
+            mTitle.setError(r.getText(R.string.bookmark_needs_title));
+            return false;
+        }
+
+        long id = addFolderToCurrent(title);
+        if (id == -1) {
+            displayToastForExistingFolder();
+            return false;
+        }
+
+        setResult(RESULT_OK);
+        return true;
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        if (mAccountSpinner == parent) {
+            long root = mAccountAdapter.getItem(position).rootFolderId;
+            if (root != mRootFolder) {
+                onRootFolderFound(root);
+                mFolderAdapter.clearRecentFolder();
+            }
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        // Don't care
+    }
+
+    static class AccountsLoader extends CursorLoader {
+
+        static final String[] PROJECTION = new String[] {
+                Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_TYPE, Accounts.ROOT_ID,
+        };
+
+        static final int COLUMN_INDEX_ACCOUNT_NAME = 0;
+
+        static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+
+        static final int COLUMN_INDEX_ROOT_ID = 2;
+
+        public AccountsLoader(Context context) {
+            super(context, Accounts.CONTENT_URI, PROJECTION, null, null, null);
+        }
+
+    }
+
+    public static class BookmarkAccount {
+
+        private String mLabel;
+
+        String mAccountName;
+        String mAccountType;
+
+        public long rootFolderId;
+
+        public BookmarkAccount(Context context, Cursor cursor) {
+            mAccountName = cursor.getString(AccountsLoader.COLUMN_INDEX_ACCOUNT_NAME);
+            mAccountType = cursor.getString(AccountsLoader.COLUMN_INDEX_ACCOUNT_TYPE);
+            rootFolderId = cursor.getLong(AccountsLoader.COLUMN_INDEX_ROOT_ID);
+            mLabel = mAccountName;
+            if (TextUtils.isEmpty(mLabel)) {
+                mLabel = context.getString(R.string.local_bookmarks);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return mLabel;
+        }
+    }
+
+    static class EditBookmarkInfo {
+        long mId = -1;
+
+        long mParentId = -1;
+
+        String mParentTitle;
+
+        String mEBITitle;
+
+        String mAccountName;
+
+        String mAccountType;
+
+        long mLastUsedId = -1;
+
+        String mLastUsedTitle;
+
+        String mLastUsedAccountName;
+
+        String mLastUsedAccountType;
+    }
+
+    static class EditBookmarkInfoLoader extends AsyncTaskLoader<EditBookmarkInfo> {
+
+        private Context mContext;
+
+        private Bundle mMap;
+
+        public EditBookmarkInfoLoader(Context context, Bundle bundle) {
+            super(context);
+            mContext = context.getApplicationContext();
+            mMap = bundle;
+        }
+
+        @Override
+        public EditBookmarkInfo loadInBackground() {
+            final ContentResolver cr = mContext.getContentResolver();
+            EditBookmarkInfo info = new EditBookmarkInfo();
+            Cursor c = null;
+            try {
+                // First, let's lookup the bookmark (check for dupes, get needed
+                // info)
+                String url = mMap.getString(BrowserContract.Bookmarks.URL);
+                info.mId = mMap.getLong(BrowserContract.Bookmarks._ID, -1);
+                boolean checkForDupe = mMap.getBoolean(CHECK_FOR_DUPE);
+                if (checkForDupe && info.mId == -1 && !TextUtils.isEmpty(url)) {
+                    c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, new String[] {
+                            BrowserContract.Bookmarks._ID
+                    }, BrowserContract.Bookmarks.URL + "=?", new String[] {
+                            url
+                    }, null);
+                    if (c.getCount() == 1 && c.moveToFirst()) {
+                        info.mId = c.getLong(0);
+                    }
+                    c.close();
+                }
+                if (info.mId != -1) {
+                    c = cr.query(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI,
+                            info.mId), new String[] {
+                            BrowserContract.Bookmarks.PARENT,
+                            BrowserContract.Bookmarks.ACCOUNT_NAME,
+                            BrowserContract.Bookmarks.ACCOUNT_TYPE, BrowserContract.Bookmarks.TITLE
+                    }, null, null, null);
+                    if (c.moveToFirst()) {
+                        info.mParentId = c.getLong(0);
+                        info.mAccountName = c.getString(1);
+                        info.mAccountType = c.getString(2);
+                        info.mEBITitle = c.getString(3);
+                    }
+                    c.close();
+                    c = cr.query(ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI,
+                            info.mParentId), new String[] {
+                            BrowserContract.Bookmarks.TITLE,
+                    }, null, null, null);
+                    if (c.moveToFirst()) {
+                        info.mParentTitle = c.getString(0);
+                    }
+                    c.close();
+                }
+
+                // Figure out the last used folder/account
+                c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, new String[] {
+                        BrowserContract.Bookmarks.PARENT,
+                }, null, null, BrowserContract.Bookmarks.DATE_MODIFIED + " DESC LIMIT 1");
+                if (c.moveToFirst()) {
+                    long parent = c.getLong(0);
+                    c.close();
+                    c = cr.query(BrowserContract.Bookmarks.CONTENT_URI, new String[] {
+                            BrowserContract.Bookmarks.TITLE,
+                            BrowserContract.Bookmarks.ACCOUNT_NAME,
+                            BrowserContract.Bookmarks.ACCOUNT_TYPE
+                    }, BrowserContract.Bookmarks._ID + "=?", new String[] {
+                            Long.toString(parent)
+                    }, null);
+                    if (c.moveToFirst()) {
+                        info.mLastUsedId = parent;
+                        info.mLastUsedTitle = c.getString(0);
+                        info.mLastUsedAccountName = c.getString(1);
+                        info.mLastUsedAccountType = c.getString(2);
+                    }
+                    c.close();
+                }
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+            return info;
+        }
+
+        @Override
+        protected void onStartLoading() {
+            forceLoad();
+        }
+    }
+}
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java
new file mode 100644
index 0000000..73a1ebf
--- /dev/null
+++ b/src/com/android/browser/AddBookmarkPage.java
@@ -0,0 +1,1257 @@
+/*
+ * Copyright (C) 2006 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.app.AlertDialog;
+import android.app.LoaderManager;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Loader;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.ParseException;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.CursorAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.browser.BrowserUtils;
+import com.android.browser.R;
+import com.android.browser.addbookmark.FolderSpinner;
+import com.android.browser.addbookmark.FolderSpinnerAdapter;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class AddBookmarkPage extends Activity
+        implements View.OnClickListener, TextView.OnEditorActionListener,
+        AdapterView.OnItemClickListener, LoaderManager.LoaderCallbacks<Cursor>,
+        BreadCrumbView.Controller, FolderSpinner.OnSetSelectionListener,
+        OnItemSelectedListener {
+
+    public static final long DEFAULT_FOLDER_ID = -1;
+    public static final String TOUCH_ICON_URL = "touch_icon_url";
+    // Place on an edited bookmark to remove the saved thumbnail
+    public static final String REMOVE_THUMBNAIL = "remove_thumbnail";
+    public static final String USER_AGENT = "user_agent";
+    public static final String CHECK_FOR_DUPE = "check_for_dupe";
+
+    /* package */ static final String EXTRA_EDIT_BOOKMARK = "bookmark";
+    /* package */ static final String EXTRA_IS_FOLDER = "is_folder";
+
+    private static final int MAX_CRUMBS_SHOWN = 1;
+
+    private final String LOGTAG = "Bookmarks";
+
+    // IDs for the CursorLoaders that are used.
+    private final int LOADER_ID_ACCOUNTS = 0;
+    private final int LOADER_ID_FOLDER_CONTENTS = 1;
+    private final int LOADER_ID_EDIT_INFO = 2;
+
+    private EditText    mTitle;
+    private EditText    mAddress;
+    private TextView    mButton;
+    private View        mCancelButton;
+    private boolean     mEditingExisting;
+    private boolean     mEditingFolder;
+    private Bundle      mMap;
+    private String      mTouchIconUrl;
+    private String      mOriginalUrl;
+    private FolderSpinner mFolder;
+    private View mDefaultView;
+    private View mFolderSelector;
+    private EditText mFolderNamer;
+    private View mFolderCancel;
+    private boolean mIsFolderNamerShowing;
+    private View mFolderNamerHolder;
+    private View mAddNewFolder;
+    private View mAddSeparator;
+    private long mCurrentFolder;
+    private FolderAdapter mAdapter;
+    private BreadCrumbView mCrumbs;
+    private TextView mFakeTitle;
+    private View mCrumbHolder;
+    private CustomListView mListView;
+    private boolean mSaveToHomeScreen;
+    private long mRootFolder;
+    private TextView mTopLevelLabel;
+    private Drawable mHeaderIcon;
+    private View mRemoveLink;
+    private View mFakeTitleHolder;
+    private FolderSpinnerAdapter mFolderAdapter;
+    private Spinner mAccountSpinner;
+    private ArrayAdapter<BookmarkAccount> mAccountAdapter;
+    // add for carrier which requires same title or address can not exist.
+    private long mDuplicateId;
+    private Context mDuplicateContext;
+
+    private static class Folder {
+        String Name;
+        long Id;
+        Folder(String name, long id) {
+            Name = name;
+            Id = id;
+        }
+    }
+
+    // Message IDs
+    private static final int SAVE_BOOKMARK = 100;
+    private static final int TOUCH_ICON_DOWNLOADED = 101;
+    private static final int BOOKMARK_DELETED = 102;
+
+    private Handler mHandler;
+
+    private InputMethodManager getInputMethodManager() {
+        return (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+    }
+
+    private Uri getUriForFolder(long folder) {
+        BookmarkAccount account =
+                (BookmarkAccount) mAccountSpinner.getSelectedItem();
+        if (folder == mRootFolder && account != null) {
+            return BookmarksLoader.addAccount(
+                    BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
+                    account.accountType, account.accountName);
+        }
+        return BrowserContract.Bookmarks.buildFolderUri(folder);
+    }
+
+    @Override
+    public void onTop(BreadCrumbView view, int level, Object data) {
+        if (null == data) return;
+        Folder folderData = (Folder) data;
+        long folder = folderData.Id;
+        LoaderManager manager = getLoaderManager();
+        CursorLoader loader = (CursorLoader) ((Loader<?>) manager.getLoader(
+                LOADER_ID_FOLDER_CONTENTS));
+        loader.setUri(getUriForFolder(folder));
+        loader.forceLoad();
+        if (mIsFolderNamerShowing) {
+            completeOrCancelFolderNaming(true);
+        }
+        setShowBookmarkIcon(level == 1);
+    }
+
+    /**
+     * Show or hide the icon for bookmarks next to "Bookmarks" in the crumb view.
+     * @param show True if the icon should visible, false otherwise.
+     */
+    private void setShowBookmarkIcon(boolean show) {
+        Drawable drawable = show ? mHeaderIcon: null;
+        mTopLevelLabel.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null);
+    }
+
+    @Override
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        if (v == mFolderNamer) {
+            if (v.getText().length() > 0) {
+                if (actionId == EditorInfo.IME_NULL) {
+                    // Only want to do this once.
+                    if (event.getAction() == KeyEvent.ACTION_UP) {
+                        completeOrCancelFolderNaming(false);
+                    }
+                }
+            }
+            // Steal the key press; otherwise a newline will be added
+            return true;
+        }
+        return false;
+    }
+
+    private void switchToDefaultView(boolean changedFolder) {
+        mFolderSelector.setVisibility(View.GONE);
+        mDefaultView.setVisibility(View.VISIBLE);
+        mCrumbHolder.setVisibility(View.GONE);
+        mFakeTitleHolder.setVisibility(View.VISIBLE);
+        if (changedFolder) {
+            Object data = mCrumbs.getTopData();
+            if (data != null) {
+                Folder folder = (Folder) data;
+                mCurrentFolder = folder.Id;
+                if (mCurrentFolder == mRootFolder) {
+                    // The Spinner changed to show "Other folder ..."  Change
+                    // it back to "Bookmarks", which is position 0 if we are
+                    // editing a folder, 1 otherwise.
+                    mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 0 : 1);
+                } else {
+                    mFolderAdapter.setOtherFolderDisplayText(folder.Name);
+                }
+            }
+        } else {
+            // The user canceled selecting a folder.  Revert back to the earlier
+            // selection.
+            if (mSaveToHomeScreen) {
+                mFolder.setSelectionIgnoringSelectionChange(0);
+            } else {
+                if (mCurrentFolder == mRootFolder) {
+                    mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 0 : 1);
+                } else {
+                    Object data = mCrumbs.getTopData();
+                    if (data != null && ((Folder) data).Id == mCurrentFolder) {
+                        // We are showing the correct folder hierarchy. The
+                        // folder selector will say "Other folder..."  Change it
+                        // to say the name of the folder once again.
+                        mFolderAdapter.setOtherFolderDisplayText(((Folder) data).Name);
+                    } else {
+                        // We are not showing the correct folder hierarchy.
+                        // Clear the Crumbs and find the proper folder
+                        setupTopCrumb();
+                        LoaderManager manager = getLoaderManager();
+                        manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+
+                    }
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v == mButton) {
+            if (mFolderSelector.getVisibility() == View.VISIBLE) {
+                // We are showing the folder selector.
+                if (mIsFolderNamerShowing) {
+                    completeOrCancelFolderNaming(false);
+                } else {
+                    // User has selected a folder.  Go back to the opening page
+                    mSaveToHomeScreen = false;
+                    switchToDefaultView(true);
+                }
+            } else {
+                // add for carrier which requires same title or address can not
+                // exist.
+                if (mSaveToHomeScreen) {
+                    if (save()) {
+                        return;
+                    }
+                } else {
+                    onSaveWithConfirm();
+                }
+            }
+        } else if (v == mCancelButton) {
+            if (mIsFolderNamerShowing) {
+                completeOrCancelFolderNaming(true);
+            } else if (mFolderSelector.getVisibility() == View.VISIBLE) {
+                switchToDefaultView(false);
+            } else {
+                finish();
+            }
+        } else if (v == mFolderCancel) {
+            completeOrCancelFolderNaming(true);
+        } else if (v == mAddNewFolder) {
+            setShowFolderNamer(true);
+            mFolderNamer.setText(R.string.new_folder);
+            mFolderNamer.requestFocus();
+            mAddNewFolder.setVisibility(View.GONE);
+            mAddSeparator.setVisibility(View.GONE);
+            InputMethodManager imm = getInputMethodManager();
+            // Set the InputMethodManager to focus on the ListView so that it
+            // can transfer the focus to mFolderNamer.
+            //imm.focusIn(mListView);
+            Object[] params  = {mListView};
+            Class[] type = new Class[] {View.class};
+            ReflectHelper.invokeMethod(imm, "focusIn", type, params);
+            imm.showSoftInput(mFolderNamer, InputMethodManager.SHOW_IMPLICIT);
+        } else if (v == mRemoveLink) {
+            if (!mEditingExisting) {
+                throw new AssertionError("Remove button should not be shown for"
+                        + " new bookmarks");
+            }
+            long id = mMap.getLong(BrowserContract.Bookmarks._ID);
+            createHandler();
+            Message msg = Message.obtain(mHandler, BOOKMARK_DELETED);
+            BookmarkUtils.displayRemoveBookmarkDialog(id,
+                    mTitle.getText().toString(), this, msg);
+        }
+    }
+
+    // FolderSpinner.OnSetSelectionListener
+
+    @Override
+    public void onSetSelection(long id) {
+        int intId = (int) id;
+        switch (intId) {
+            case FolderSpinnerAdapter.ROOT_FOLDER:
+                mCurrentFolder = mRootFolder;
+                mSaveToHomeScreen = false;
+                break;
+            case FolderSpinnerAdapter.HOME_SCREEN:
+                // Create a short cut to the home screen
+                mSaveToHomeScreen = true;
+                break;
+            case FolderSpinnerAdapter.OTHER_FOLDER:
+                switchToFolderSelector();
+                break;
+            case FolderSpinnerAdapter.RECENT_FOLDER:
+                mCurrentFolder = mFolderAdapter.recentFolderId();
+                mSaveToHomeScreen = false;
+                // In case the user decides to select OTHER_FOLDER
+                // and choose a different one, so that we will start from
+                // the correct place.
+                LoaderManager manager = getLoaderManager();
+                manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+                break;
+            default:
+                break;
+        }
+    }
+
+    /**
+     * Finish naming a folder, and close the IME
+     * @param cancel If true, the new folder is not created.  If false, the new
+     *      folder is created and the user is taken inside it.
+     */
+    private void completeOrCancelFolderNaming(boolean cancel) {
+        if (!cancel && !TextUtils.isEmpty(mFolderNamer.getText())) {
+            String name = mFolderNamer.getText().toString();
+            long id = addFolderToCurrent(mFolderNamer.getText().toString());
+            descendInto(name, id);
+        }
+        setShowFolderNamer(false);
+        mAddNewFolder.setVisibility(View.VISIBLE);
+        mAddSeparator.setVisibility(View.VISIBLE);
+        getInputMethodManager().hideSoftInputFromWindow(
+                mListView.getWindowToken(), 0);
+    }
+
+    private long addFolderToCurrent(String name) {
+        // Add the folder to the database
+        ContentValues values = new ContentValues();
+        values.put(BrowserContract.Bookmarks.TITLE,
+                name);
+        values.put(BrowserContract.Bookmarks.IS_FOLDER, 1);
+        long currentFolder;
+        Object data = mCrumbs.getTopData();
+        if (data != null) {
+            currentFolder = ((Folder) data).Id;
+        } else {
+            currentFolder = mRootFolder;
+        }
+        values.put(BrowserContract.Bookmarks.PARENT, currentFolder);
+        Uri uri = getContentResolver().insert(
+                BrowserContract.Bookmarks.CONTENT_URI, values);
+        if (uri != null) {
+            return ContentUris.parseId(uri);
+        } else {
+            return -1;
+        }
+    }
+
+    private void switchToFolderSelector() {
+        // Set the list to the top in case it is scrolled.
+        mListView.setSelection(0);
+        mDefaultView.setVisibility(View.GONE);
+        mFolderSelector.setVisibility(View.VISIBLE);
+        mCrumbHolder.setVisibility(View.VISIBLE);
+        mFakeTitleHolder.setVisibility(View.GONE);
+        mAddNewFolder.setVisibility(View.VISIBLE);
+        mAddSeparator.setVisibility(View.VISIBLE);
+        getInputMethodManager().hideSoftInputFromWindow(
+                mListView.getWindowToken(), 0);
+    }
+
+    private void descendInto(String foldername, long id) {
+        if (id != DEFAULT_FOLDER_ID) {
+            mCrumbs.pushView(foldername, new Folder(foldername, id));
+            mCrumbs.notifyController();
+        }
+    }
+
+    private LoaderCallbacks<EditBookmarkInfo> mEditInfoLoaderCallbacks =
+            new LoaderCallbacks<EditBookmarkInfo>() {
+
+        @Override
+        public void onLoaderReset(Loader<EditBookmarkInfo> loader) {
+            // Don't care
+        }
+
+        @Override
+        public void onLoadFinished(Loader<EditBookmarkInfo> loader,
+                EditBookmarkInfo info) {
+            boolean setAccount = false;
+            if (info.id != -1) {
+                mEditingExisting = true;
+                showRemoveButton();
+                mFakeTitle.setText(R.string.edit_bookmark);
+                mTitle.setText(info.title);
+                mFolderAdapter.setOtherFolderDisplayText(info.parentTitle);
+                mMap.putLong(BrowserContract.Bookmarks._ID, info.id);
+                setAccount = true;
+                setAccount(info.accountName, info.accountType);
+                mCurrentFolder = info.parentId;
+                onCurrentFolderFound();
+            }
+            // TODO: Detect if lastUsedId is a subfolder of info.id in the
+            // editing folder case. For now, just don't show the last used
+            // folder at all to prevent any chance of the user adding a parent
+            // folder to a child folder
+            if (info.lastUsedId != -1 && info.lastUsedId != info.id
+                    && !mEditingFolder) {
+                if (setAccount && info.lastUsedId != mRootFolder
+                        && TextUtils.equals(info.lastUsedAccountName, info.accountName)
+                        && TextUtils.equals(info.lastUsedAccountType, info.accountType)) {
+                    mFolderAdapter.addRecentFolder(info.lastUsedId, info.lastUsedTitle);
+                } else if (!setAccount) {
+                    setAccount = true;
+                    setAccount(info.lastUsedAccountName, info.lastUsedAccountType);
+                    if (info.lastUsedId != mRootFolder) {
+                        mFolderAdapter.addRecentFolder(info.lastUsedId,
+                                info.lastUsedTitle);
+                    }
+                }
+            }
+            if (!setAccount) {
+                mAccountSpinner.setSelection(0);
+            }
+        }
+
+        @Override
+        public Loader<EditBookmarkInfo> onCreateLoader(int id, Bundle args) {
+            return new EditBookmarkInfoLoader(AddBookmarkPage.this, mMap);
+        }
+    };
+
+    void setAccount(String accountName, String accountType) {
+        for (int i = 0; i < mAccountAdapter.getCount(); i++) {
+            BookmarkAccount account = mAccountAdapter.getItem(i);
+            if (TextUtils.equals(account.accountName, accountName)
+                    && TextUtils.equals(account.accountType, accountType)) {
+                mAccountSpinner.setSelection(i);
+                onRootFolderFound(account.rootFolderId);
+                return;
+            }
+        }
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        String[] projection;
+        switch (id) {
+            case LOADER_ID_ACCOUNTS:
+                return new AccountsLoader(this);
+            case LOADER_ID_FOLDER_CONTENTS:
+                projection = new String[] {
+                        BrowserContract.Bookmarks._ID,
+                        BrowserContract.Bookmarks.TITLE,
+                        BrowserContract.Bookmarks.IS_FOLDER
+                };
+                String where = BrowserContract.Bookmarks.IS_FOLDER + " != 0";
+                String whereArgs[] = null;
+                if (mEditingFolder) {
+                    where += " AND " + BrowserContract.Bookmarks._ID + " != ?";
+                    whereArgs = new String[] { Long.toString(mMap.getLong(
+                            BrowserContract.Bookmarks._ID)) };
+                }
+                long currentFolder;
+                Object data = mCrumbs.getTopData();
+                if (data != null) {
+                    currentFolder = ((Folder) data).Id;
+                } else {
+                    currentFolder = mRootFolder;
+                }
+                return new CursorLoader(this,
+                        getUriForFolder(currentFolder),
+                        projection,
+                        where,
+                        whereArgs,
+                        BrowserContract.Bookmarks._ID + " ASC");
+            default:
+                throw new AssertionError("Asking for nonexistant loader!");
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        switch (loader.getId()) {
+            case LOADER_ID_ACCOUNTS:
+                mAccountAdapter.clear();
+                while (cursor.moveToNext()) {
+                    mAccountAdapter.add(new BookmarkAccount(this, cursor));
+                }
+                getLoaderManager().destroyLoader(LOADER_ID_ACCOUNTS);
+                getLoaderManager().restartLoader(LOADER_ID_EDIT_INFO, null,
+                        mEditInfoLoaderCallbacks);
+                break;
+            case LOADER_ID_FOLDER_CONTENTS:
+                mAdapter.changeCursor(cursor);
+                break;
+        }
+    }
+
+    public void onLoaderReset(Loader<Cursor> loader) {
+        switch (loader.getId()) {
+            case LOADER_ID_FOLDER_CONTENTS:
+                mAdapter.changeCursor(null);
+                break;
+        }
+    }
+
+    /**
+     * Move cursor to the position that has folderToFind as its "_id".
+     * @param cursor Cursor containing folders in the bookmarks database
+     * @param folderToFind "_id" of the folder to move to.
+     * @param idIndex Index in cursor of "_id"
+     * @throws AssertionError if cursor is empty or there is no row with folderToFind
+     *      as its "_id".
+     */
+    void moveCursorToFolder(Cursor cursor, long folderToFind, int idIndex)
+            throws AssertionError {
+        if (!cursor.moveToFirst()) {
+            throw new AssertionError("No folders in the database!");
+        }
+        long folder;
+        do {
+            folder = cursor.getLong(idIndex);
+        } while (folder != folderToFind && cursor.moveToNext());
+        if (cursor.isAfterLast()) {
+            throw new AssertionError("Folder(id=" + folderToFind
+                    + ") holding this bookmark does not exist!");
+        }
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position,
+            long id) {
+        TextView tv = (TextView) view.findViewById(android.R.id.text1);
+        // Switch to the folder that was clicked on.
+        descendInto(tv.getText().toString(), id);
+    }
+
+    private void setShowFolderNamer(boolean show) {
+        if (show != mIsFolderNamerShowing) {
+            mIsFolderNamerShowing = show;
+            if (show) {
+                // Set the selection to the folder namer so it will be in
+                // view.
+                mListView.addFooterView(mFolderNamerHolder);
+            } else {
+                mListView.removeFooterView(mFolderNamerHolder);
+            }
+            // Refresh the list.
+            mListView.setAdapter(mAdapter);
+            if (show) {
+                mListView.setSelection(mListView.getCount() - 1);
+            }
+        }
+    }
+
+    /**
+     * Shows a list of names of folders.
+     */
+    private class FolderAdapter extends CursorAdapter {
+        public FolderAdapter(Context context) {
+            super(context, null);
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            ((TextView) view.findViewById(android.R.id.text1)).setText(
+                    cursor.getString(cursor.getColumnIndexOrThrow(
+                    BrowserContract.Bookmarks.TITLE)));
+        }
+
+        @Override
+        public View newView(Context context, Cursor cursor, ViewGroup parent) {
+            View view = LayoutInflater.from(context).inflate(
+                    R.layout.folder_list_item, null);
+            view.setBackgroundDrawable(context.getResources().
+                    getDrawable(android.R.drawable.list_selector_background));
+            return view;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            // Do not show the empty view if the user is creating a new folder.
+            return super.isEmpty() && !mIsFolderNamerShowing;
+        }
+    }
+
+    @Override
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+        mMap = getIntent().getExtras();
+
+        setContentView(R.layout.browser_add_bookmark);
+
+        Window window = getWindow();
+
+        String title = null;
+        String url = null;
+
+        mFakeTitle = (TextView) findViewById(R.id.fake_title);
+
+        if (mMap != null) {
+            Bundle b = mMap.getBundle(EXTRA_EDIT_BOOKMARK);
+            if (b != null) {
+                mEditingFolder = mMap.getBoolean(EXTRA_IS_FOLDER, false);
+                mMap = b;
+                mEditingExisting = true;
+                mFakeTitle.setText(R.string.edit_bookmark);
+                if (mEditingFolder) {
+                    findViewById(R.id.row_address).setVisibility(View.GONE);
+                } else {
+                    showRemoveButton();
+                }
+            } else {
+                int gravity = mMap.getInt("gravity", -1);
+                if (gravity != -1) {
+                    WindowManager.LayoutParams l = window.getAttributes();
+                    l.gravity = gravity;
+                    window.setAttributes(l);
+                }
+            }
+            title = mMap.getString(BrowserContract.Bookmarks.TITLE);
+            url = mOriginalUrl = mMap.getString(BrowserContract.Bookmarks.URL);
+            mTouchIconUrl = mMap.getString(TOUCH_ICON_URL);
+            mCurrentFolder = mMap.getLong(BrowserContract.Bookmarks.PARENT, DEFAULT_FOLDER_ID);
+        }
+
+        mTitle = (EditText) findViewById(R.id.title);
+        mTitle.setText(title);
+        BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mTitle, BrowserUtils.FILENAME_MAX_LENGTH);
+
+        mAddress = (EditText) findViewById(R.id.address);
+        mAddress.setText(url);
+        BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mAddress, BrowserUtils.ADDRESS_MAX_LENGTH);
+
+        mButton = (TextView) findViewById(R.id.OK);
+        mButton.setOnClickListener(this);
+
+        mCancelButton = findViewById(R.id.cancel);
+        mCancelButton.setOnClickListener(this);
+
+        mFolder = (FolderSpinner) findViewById(R.id.folder);
+        mFolderAdapter = new FolderSpinnerAdapter(this, !mEditingFolder);
+        mFolder.setAdapter(mFolderAdapter);
+        mFolder.setOnSetSelectionListener(this);
+
+        mDefaultView = findViewById(R.id.default_view);
+        mFolderSelector = findViewById(R.id.folder_selector);
+
+        mFolderNamerHolder = getLayoutInflater().inflate(R.layout.new_folder_layout, null);
+        mFolderNamer = (EditText) mFolderNamerHolder.findViewById(R.id.folder_namer);
+        mFolderNamer.setOnEditorActionListener(this);
+
+        // add for carrier test about warning limit of edit text
+        BrowserUtils.maxLengthFilter(AddBookmarkPage.this, mFolderNamer,
+                BrowserUtils.FILENAME_MAX_LENGTH);
+
+        mFolderCancel = mFolderNamerHolder.findViewById(R.id.close);
+        mFolderCancel.setOnClickListener(this);
+
+        mAddNewFolder = findViewById(R.id.add_new_folder);
+        mAddNewFolder.setOnClickListener(this);
+        mAddSeparator = findViewById(R.id.add_divider);
+
+        mCrumbs = (BreadCrumbView) findViewById(R.id.crumbs);
+        mCrumbs.setUseBackButton(true);
+        mCrumbs.setController(this);
+        mHeaderIcon = getResources().getDrawable(R.drawable.ic_folder_holo_dark);
+        mCrumbHolder = findViewById(R.id.crumb_holder);
+        mCrumbs.setMaxVisible(MAX_CRUMBS_SHOWN);
+
+        mAdapter = new FolderAdapter(this);
+        mListView = (CustomListView) findViewById(R.id.list);
+        View empty = findViewById(R.id.empty);
+        mListView.setEmptyView(empty);
+        mListView.setAdapter(mAdapter);
+        mListView.setOnItemClickListener(this);
+        mListView.addEditText(mFolderNamer);
+
+        mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this,
+                android.R.layout.simple_spinner_item);
+        mAccountAdapter.setDropDownViewResource(
+                android.R.layout.simple_spinner_dropdown_item);
+        mAccountSpinner = (Spinner) findViewById(R.id.accounts);
+        mAccountSpinner.setAdapter(mAccountAdapter);
+        mAccountSpinner.setOnItemSelectedListener(this);
+
+
+        mFakeTitleHolder = findViewById(R.id.title_holder);
+
+        if (!window.getDecorView().isInTouchMode()) {
+            mButton.requestFocus();
+        }
+
+        getLoaderManager().restartLoader(LOADER_ID_ACCOUNTS, null, this);
+    }
+
+    private void showRemoveButton() {
+        findViewById(R.id.remove_divider).setVisibility(View.VISIBLE);
+        mRemoveLink = findViewById(R.id.remove);
+        mRemoveLink.setVisibility(View.VISIBLE);
+        mRemoveLink.setOnClickListener(this);
+    }
+
+    // Called once we have determined which folder is the root folder
+    private void onRootFolderFound(long root) {
+        mRootFolder = root;
+        mCurrentFolder = mRootFolder;
+        setupTopCrumb();
+        onCurrentFolderFound();
+    }
+
+    private void setupTopCrumb() {
+        mCrumbs.clear();
+        String name = getString(R.string.bookmarks);
+        mTopLevelLabel = (TextView) mCrumbs.pushView(name, false,
+                new Folder(name, mRootFolder));
+        // To better match the other folders.
+        mTopLevelLabel.setCompoundDrawablePadding(6);
+    }
+
+    private void onCurrentFolderFound() {
+        LoaderManager manager = getLoaderManager();
+        if (mCurrentFolder != mRootFolder) {
+            // Since we're not in the root folder, change the selection to other
+            // folder now.  The text will get changed once we select the correct
+            // folder.
+            mFolder.setSelectionIgnoringSelectionChange(mEditingFolder ? 1 : 2);
+        } else {
+            setShowBookmarkIcon(true);
+            if (!mEditingFolder) {
+                // Initially the "Bookmarks" folder should be showing, rather than
+                // the home screen.  In the editing folder case, home screen is not
+                // an option, so "Bookmarks" folder is already at the top.
+                mFolder.setSelectionIgnoringSelectionChange(FolderSpinnerAdapter.ROOT_FOLDER);
+            }
+        }
+        // Find the contents of the current folder
+        manager.restartLoader(LOADER_ID_FOLDER_CONTENTS, null, this);
+    }
+
+    /**
+     * Runnable to save a bookmark, so it can be performed in its own thread.
+     */
+    private class SaveBookmarkRunnable implements Runnable {
+        // FIXME: This should be an async task.
+        private Message mMessage;
+        private Context mContext;
+        public SaveBookmarkRunnable(Context ctx, Message msg) {
+            mContext = ctx.getApplicationContext();
+            mMessage = msg;
+        }
+        public void run() {
+            // Unbundle bookmark data.
+            Bundle bundle = mMessage.getData();
+            String title = bundle.getString(BrowserContract.Bookmarks.TITLE);
+            String url = bundle.getString(BrowserContract.Bookmarks.URL);
+            boolean invalidateThumbnail = bundle.getBoolean(REMOVE_THUMBNAIL);
+            Bitmap thumbnail = invalidateThumbnail ? null
+                    : (Bitmap) bundle.getParcelable(BrowserContract.Bookmarks.THUMBNAIL);
+            String touchIconUrl = bundle.getString(TOUCH_ICON_URL);
+
+            // Save to the bookmarks DB.
+            try {
+                final ContentResolver cr = getContentResolver();
+                Bookmarks.addBookmark(AddBookmarkPage.this, false, url,
+                        title, thumbnail, mCurrentFolder);
+                if (touchIconUrl != null) {
+                    new DownloadTouchIcon(mContext, cr, url).execute(mTouchIconUrl);
+                }
+                mMessage.arg1 = 1;
+            } catch (IllegalStateException e) {
+                mMessage.arg1 = 0;
+            }
+            mMessage.sendToTarget();
+        }
+    }
+
+    private static class UpdateBookmarkTask extends AsyncTask<ContentValues, Void, Void> {
+        Context mContext;
+        Long mId;
+
+        public UpdateBookmarkTask(Context context, long id) {
+            mContext = context.getApplicationContext();
+            mId = id;
+        }
+
+        @Override
+        protected Void doInBackground(ContentValues... params) {
+            if (params.length != 1) {
+                throw new IllegalArgumentException("No ContentValues provided!");
+            }
+            Uri uri = ContentUris.withAppendedId(BookmarkUtils.getBookmarksUri(mContext), mId);
+            mContext.getContentResolver().update(
+                    uri,
+                    params[0], null, null);
+            return null;
+        }
+    }
+
+    private void createHandler() {
+        if (mHandler == null) {
+            mHandler = new Handler() {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case SAVE_BOOKMARK:
+                            if (1 == msg.arg1) {
+                                Toast.makeText(AddBookmarkPage.this, R.string.bookmark_saved,
+                                        Toast.LENGTH_LONG).show();
+                            } else {
+                                Toast.makeText(AddBookmarkPage.this, R.string.bookmark_not_saved,
+                                        Toast.LENGTH_LONG).show();
+                            }
+                            break;
+                        case TOUCH_ICON_DOWNLOADED:
+                            Bundle b = msg.getData();
+                            sendBroadcast(BookmarkUtils.createAddToHomeIntent(
+                                    AddBookmarkPage.this,
+                                    b.getString(BrowserContract.Bookmarks.URL),
+                                    b.getString(BrowserContract.Bookmarks.TITLE),
+                                    (Bitmap) b.getParcelable(BrowserContract.Bookmarks.TOUCH_ICON),
+                                    (Bitmap) b.getParcelable(BrowserContract.Bookmarks.FAVICON)));
+                            break;
+                        case BOOKMARK_DELETED:
+                            finish();
+                            break;
+                    }
+                }
+            };
+        }
+    }
+
+    static void deleteDuplicateBookmark(final Context context, final long id) {
+        Uri uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI, id);
+        context.getContentResolver().delete(uri, null, null);
+    }
+
+    private void onSaveWithConfirm() {
+        String title = mTitle.getText().toString().trim();
+        String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+        String url = unfilteredUrl.trim();
+        Long id = mMap.getLong(BrowserContract.Bookmarks._ID);
+        int duplicateCount;
+        final ContentResolver cr = getContentResolver();
+
+        Cursor cursor = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+                BookmarksLoader.PROJECTION,
+                "( title = ? OR url = ? ) AND parent = ?",
+                new String[] {
+                        title, url, Long.toString(mCurrentFolder)
+                },
+                null);
+
+        if (cursor == null) {
+            save();
+            return;
+        }
+
+        duplicateCount = cursor.getCount();
+        if (duplicateCount <= 0) {
+            cursor.close();
+            save();
+            return;
+        } else {
+            try {
+                while (cursor.moveToNext()) {
+                    mDuplicateId = cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+                    mDuplicateContext = AddBookmarkPage.this;
+                }
+            } catch (IllegalStateException e) {
+                e.printStackTrace();
+            } finally {
+                if (cursor != null)
+                    cursor.close();
+            }
+        }
+
+        if (mEditingExisting && duplicateCount == 1 && mDuplicateId == id) {
+            save();
+            return;
+        }
+
+        new AlertDialog.Builder(this)
+                .setTitle(getString(R.string.save_to_bookmarks_title))
+                .setMessage(getString(R.string.overwrite_bookmark_msg))
+                .setNegativeButton(android.R.string.cancel, null)
+                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        if (mDuplicateContext == null) {
+                            return;
+                        }
+                        deleteDuplicateBookmark(mDuplicateContext, mDuplicateId);
+                        save();
+                    }
+                })
+                .show();
+    }
+
+    /**
+     * Parse the data entered in the dialog and post a message to update the bookmarks database.
+     */
+    boolean save() {
+        createHandler();
+
+        String title = mTitle.getText().toString().trim();
+        String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+
+        boolean emptyTitle = title.length() == 0;
+        boolean emptyUrl = unfilteredUrl.trim().length() == 0;
+        Resources r = getResources();
+        if (emptyTitle || (emptyUrl && !mEditingFolder)) {
+            if (emptyTitle) {
+                mTitle.setError(r.getText(R.string.bookmark_needs_title));
+            }
+            if (emptyUrl) {
+                mAddress.setError(r.getText(R.string.bookmark_needs_url));
+            }
+            return false;
+        }
+        String url = unfilteredUrl.trim();
+        if (!mEditingFolder) {
+            try {
+                // We allow bookmarks with a javascript: scheme, but these will in most cases
+                // fail URI parsing, so don't try it if that's the kind of bookmark we have.
+
+                if (!url.toLowerCase().startsWith("javascript:")) {
+                    URI uriObj = new URI(url);
+                    String scheme = uriObj.getScheme();
+                    if (!Bookmarks.urlHasAcceptableScheme(url)) {
+                        // If the scheme was non-null, let the user know that we
+                        // can't save their bookmark. If it was null, we'll assume
+                        // they meant http when we parse it in the WebAddress class.
+                        if (scheme != null) {
+                            mAddress.setError(r.getText(R.string.bookmark_cannot_save_url));
+                            return false;
+                        }
+                        WebAddress address;
+                        try {
+                            address = new WebAddress(unfilteredUrl);
+                        } catch (ParseException e) {
+                            throw new URISyntaxException("", "");
+                        }
+                        if (address.getHost().length() == 0) {
+                            throw new URISyntaxException("", "");
+                        }
+                        url = address.toString();
+                    }
+                }
+            } catch (URISyntaxException e) {
+                mAddress.setError(r.getText(R.string.bookmark_url_not_valid));
+                return false;
+            }
+        }
+
+        if (mSaveToHomeScreen) {
+            mEditingExisting = false;
+        }
+
+        boolean urlUnmodified = url.equals(mOriginalUrl);
+
+        if (mEditingExisting) {
+            Long id = mMap.getLong(BrowserContract.Bookmarks._ID);
+            ContentValues values = new ContentValues();
+            values.put(BrowserContract.Bookmarks.TITLE, title);
+            values.put(BrowserContract.Bookmarks.PARENT, mCurrentFolder);
+            if (!mEditingFolder) {
+                values.put(BrowserContract.Bookmarks.URL, url);
+                if (!urlUnmodified) {
+                    values.putNull(BrowserContract.Bookmarks.THUMBNAIL);
+                }
+            }
+            if (values.size() > 0) {
+                new UpdateBookmarkTask(getApplicationContext(), id).execute(values);
+            }
+            setResult(RESULT_OK);
+        } else {
+            Bitmap thumbnail;
+            Bitmap favicon;
+            if (urlUnmodified) {
+                thumbnail = (Bitmap) mMap.getParcelable(
+                        BrowserContract.Bookmarks.THUMBNAIL);
+                favicon = (Bitmap) mMap.getParcelable(
+                        BrowserContract.Bookmarks.FAVICON);
+            } else {
+                thumbnail = null;
+                favicon = null;
+            }
+
+            Bundle bundle = new Bundle();
+            bundle.putString(BrowserContract.Bookmarks.TITLE, title);
+            bundle.putString(BrowserContract.Bookmarks.URL, url);
+            bundle.putParcelable(BrowserContract.Bookmarks.FAVICON, favicon);
+
+            if (mSaveToHomeScreen) {
+                if (mTouchIconUrl != null && urlUnmodified) {
+                    Message msg = Message.obtain(mHandler,
+                            TOUCH_ICON_DOWNLOADED);
+                    msg.setData(bundle);
+                    DownloadTouchIcon icon = new DownloadTouchIcon(this, msg,
+                            mMap.getString(USER_AGENT));
+                    icon.execute(mTouchIconUrl);
+                } else {
+                    sendBroadcast(BookmarkUtils.createAddToHomeIntent(this, url,
+                            title, null /*touchIcon*/, favicon));
+                }
+            } else {
+                bundle.putParcelable(BrowserContract.Bookmarks.THUMBNAIL, thumbnail);
+                bundle.putBoolean(REMOVE_THUMBNAIL, !urlUnmodified);
+                bundle.putString(TOUCH_ICON_URL, mTouchIconUrl);
+                // Post a message to write to the DB.
+                Message msg = Message.obtain(mHandler, SAVE_BOOKMARK);
+                msg.setData(bundle);
+                // Start a new thread so as to not slow down the UI
+                Thread t = new Thread(new SaveBookmarkRunnable(getApplicationContext(), msg));
+                t.start();
+            }
+            setResult(RESULT_OK);
+            LogTag.logBookmarkAdded(url, "bookmarkview");
+        }
+        finish();
+        return true;
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position,
+            long id) {
+        if (mAccountSpinner == parent) {
+            long root = mAccountAdapter.getItem(position).rootFolderId;
+            if (root != mRootFolder) {
+                onRootFolderFound(root);
+                mFolderAdapter.clearRecentFolder();
+            }
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {
+        // Don't care
+    }
+
+    /*
+     * Class used as a proxy for the InputMethodManager to get to mFolderNamer
+     */
+    public static class CustomListView extends ListView {
+        private EditText mEditText;
+
+        public void addEditText(EditText editText) {
+            mEditText = editText;
+        }
+
+        public CustomListView(Context context) {
+            super(context);
+        }
+
+        public CustomListView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        public CustomListView(Context context, AttributeSet attrs, int defStyle) {
+            super(context, attrs, defStyle);
+        }
+
+        @Override
+        public boolean checkInputConnectionProxy(View view) {
+            return view == mEditText;
+        }
+    }
+
+    static class AccountsLoader extends CursorLoader {
+
+        static final String[] PROJECTION = new String[] {
+            Accounts.ACCOUNT_NAME,
+            Accounts.ACCOUNT_TYPE,
+            Accounts.ROOT_ID,
+        };
+
+        static final int COLUMN_INDEX_ACCOUNT_NAME = 0;
+        static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+        static final int COLUMN_INDEX_ROOT_ID = 2;
+
+        public AccountsLoader(Context context) {
+            super(context, Accounts.CONTENT_URI, PROJECTION, null, null, null);
+        }
+
+    }
+
+    public static class BookmarkAccount {
+
+        private String mLabel;
+        String accountName, accountType;
+        public long rootFolderId;
+
+        public BookmarkAccount(Context context, Cursor cursor) {
+            accountName = cursor.getString(
+                    AccountsLoader.COLUMN_INDEX_ACCOUNT_NAME);
+            accountType = cursor.getString(
+                    AccountsLoader.COLUMN_INDEX_ACCOUNT_TYPE);
+            rootFolderId = cursor.getLong(
+                    AccountsLoader.COLUMN_INDEX_ROOT_ID);
+            mLabel = accountName;
+            if (TextUtils.isEmpty(mLabel)) {
+                mLabel = context.getString(R.string.local_bookmarks);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return mLabel;
+        }
+    }
+
+    static class EditBookmarkInfo {
+        long id = -1;
+        long parentId = -1;
+        String parentTitle;
+        String title;
+        String accountName;
+        String accountType;
+
+        long lastUsedId = -1;
+        String lastUsedTitle;
+        String lastUsedAccountName;
+        String lastUsedAccountType;
+    }
+
+    static class EditBookmarkInfoLoader extends AsyncTaskLoader<EditBookmarkInfo> {
+
+        private Context mContext;
+        private Bundle mMap;
+
+        public EditBookmarkInfoLoader(Context context, Bundle bundle) {
+            super(context);
+            mContext = context.getApplicationContext();
+            mMap = bundle;
+        }
+
+        @Override
+        public EditBookmarkInfo loadInBackground() {
+            final ContentResolver cr = mContext.getContentResolver();
+            EditBookmarkInfo info = new EditBookmarkInfo();
+            Cursor c = null;
+
+            try {
+                // First, let's lookup the bookmark (check for dupes, get needed info)
+                String url = mMap.getString(BrowserContract.Bookmarks.URL);
+                info.id = mMap.getLong(BrowserContract.Bookmarks._ID, -1);
+                boolean checkForDupe = mMap.getBoolean(CHECK_FOR_DUPE);
+                if (checkForDupe && info.id == -1 && !TextUtils.isEmpty(url)) {
+                    c = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+                            new String[] { BrowserContract.Bookmarks._ID},
+                            BrowserContract.Bookmarks.URL + "=?",
+                            new String[] { url }, null);
+                    if (c.getCount() == 1 && c.moveToFirst()) {
+                        info.id = c.getLong(0);
+                    }
+                    c.close();
+                }
+                if (info.id != -1) {
+                    c = cr.query(ContentUris.withAppendedId(
+                            BrowserContract.Bookmarks.CONTENT_URI, info.id),
+                            new String[] {
+                            BrowserContract.Bookmarks.PARENT,
+                            BrowserContract.Bookmarks.ACCOUNT_NAME,
+                            BrowserContract.Bookmarks.ACCOUNT_TYPE,
+                            BrowserContract.Bookmarks.TITLE},
+                            null, null, null);
+                    if (c.moveToFirst()) {
+                        info.parentId = c.getLong(0);
+                        info.accountName = c.getString(1);
+                        info.accountType = c.getString(2);
+                        info.title = c.getString(3);
+                    }
+                    c.close();
+                    c = cr.query(ContentUris.withAppendedId(
+                            BrowserContract.Bookmarks.CONTENT_URI, info.parentId),
+                            new String[] {
+                            BrowserContract.Bookmarks.TITLE,},
+                            null, null, null);
+                    if (c.moveToFirst()) {
+                        info.parentTitle = c.getString(0);
+                    }
+                    c.close();
+                }
+
+                // Figure out the last used folder/account
+                c = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+                        new String[] {
+                        BrowserContract.Bookmarks.PARENT,
+                        }, null, null,
+                        BrowserContract.Bookmarks.DATE_MODIFIED + " DESC LIMIT 1");
+                if (c.moveToFirst()) {
+                    long parent = c.getLong(0);
+                    c.close();
+                    c = cr.query(BrowserContract.Bookmarks.CONTENT_URI,
+                            new String[] {
+                            BrowserContract.Bookmarks.TITLE,
+                            BrowserContract.Bookmarks.ACCOUNT_NAME,
+                            BrowserContract.Bookmarks.ACCOUNT_TYPE},
+                            BrowserContract.Bookmarks._ID + "=?", new String[] {
+                            Long.toString(parent)}, null);
+                    if (c.moveToFirst()) {
+                        info.lastUsedId = parent;
+                        info.lastUsedTitle = c.getString(0);
+                        info.lastUsedAccountName = c.getString(1);
+                        info.lastUsedAccountType = c.getString(2);
+                    }
+                    c.close();
+                }
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+
+            return info;
+        }
+
+        @Override
+        protected void onStartLoading() {
+            forceLoad();
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/AddNewBookmark.java b/src/com/android/browser/AddNewBookmark.java
new file mode 100644
index 0000000..5decb65
--- /dev/null
+++ b/src/com/android/browser/AddNewBookmark.java
@@ -0,0 +1,55 @@
+/*
+ * 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.Context;
+import android.view.LayoutInflater;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.browser.R;
+
+/**
+ *  Custom layout for an item representing a bookmark in the browser.
+ */
+ // FIXME: Remove BrowserBookmarkItem
+class AddNewBookmark extends LinearLayout {
+
+    private TextView    mUrlText;
+
+    /**
+     *  Instantiate a bookmark item, including a default favicon.
+     *
+     *  @param context  The application context for the item.
+     */
+    AddNewBookmark(Context context) {
+        super(context);
+
+        setWillNotDraw(false);
+        LayoutInflater factory = LayoutInflater.from(context);
+        factory.inflate(R.layout.add_new_bookmark, this);
+        mUrlText = (TextView) findViewById(R.id.url);
+    }
+
+    /**
+     *  Set the new url for the bookmark item.
+     *  @param url  The new url for the bookmark item.
+     */
+    /* package */ void setUrl(String url) {
+        mUrlText.setText(url);
+    }
+}
diff --git a/src/com/android/browser/AutoFillSettingsFragment.java b/src/com/android/browser/AutoFillSettingsFragment.java
new file mode 100644
index 0000000..e87cb89
--- /dev/null
+++ b/src/com/android/browser/AutoFillSettingsFragment.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2010 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 org.codeaurora.swe.AutoFillProfile;
+
+import com.android.browser.R;
+
+import android.app.Fragment;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+public class AutoFillSettingsFragment extends Fragment {
+
+    private static final String LOGTAG = "AutoFillSettingsFragment";
+
+    private EditText mFullNameEdit;
+    private EditText mEmailEdit;
+    private EditText mCompanyEdit;
+    private EditText mAddressLine1Edit;
+    private EditText mAddressLine2Edit;
+    private EditText mCityEdit;
+    private EditText mStateEdit;
+    private EditText mZipEdit;
+    private EditText mCountryEdit;
+    private EditText mPhoneEdit;
+
+    private MenuItem mSaveMenuItem;
+
+    private boolean mInitialised;
+
+    // Used to display toast after DB interactions complete.
+    private Handler mHandler;
+    private BrowserSettings mSettings;
+
+    private final static int PROFILE_SAVED_MSG = 100;
+    private final static int PROFILE_DELETED_MSG = 101;
+
+    // For now we support just one profile so it's safe to hardcode the
+    // id to 1 here. In the future this unique identifier will be set
+    // dynamically.
+
+    private class PhoneNumberValidator implements TextWatcher {
+        // Keep in sync with kPhoneNumberLength in chrome/browser/autofill/phone_number.cc
+        private static final int PHONE_NUMBER_LENGTH = 7;
+        private static final String PHONE_NUMBER_SEPARATORS_REGEX = "[\\s\\.\\(\\)-]";
+
+        public void afterTextChanged(Editable s) {
+            String phoneNumber = s.toString();
+            int phoneNumberLength = phoneNumber.length();
+
+            // Strip out any phone number separators.
+            phoneNumber = phoneNumber.replaceAll(PHONE_NUMBER_SEPARATORS_REGEX, "");
+
+            int strippedPhoneNumberLength = phoneNumber.length();
+
+            if (phoneNumberLength > 0 && strippedPhoneNumberLength < PHONE_NUMBER_LENGTH) {
+                mPhoneEdit.setError(getResources().getText(
+                        R.string.autofill_profile_editor_phone_number_invalid));
+            } else {
+                mPhoneEdit.setError(null);
+            }
+
+            updateSaveMenuItemState();
+        }
+
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        }
+
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+        }
+    }
+
+    private class FieldChangedListener implements TextWatcher {
+        public void afterTextChanged(Editable s) {
+            updateSaveMenuItemState();
+        }
+
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+        }
+
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+        }
+
+    }
+
+    private TextWatcher mFieldChangedListener = new FieldChangedListener();
+
+    public AutoFillSettingsFragment() {
+        mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                Context c = getActivity();
+                switch (msg.what) {
+                case PROFILE_SAVED_MSG:
+                    if (c != null) {
+                        Toast.makeText(c, R.string.autofill_profile_successful_save,
+                                Toast.LENGTH_SHORT).show();
+                        closeEditor();
+                    }
+                    break;
+
+                case PROFILE_DELETED_MSG:
+                    if (c != null) {
+                        Toast.makeText(c, R.string.autofill_profile_successful_delete,
+                                Toast.LENGTH_SHORT).show();
+                    }
+                    break;
+                }
+            }
+        };
+    }
+
+    @Override
+    public void onCreate(Bundle savedState) {
+        super.onCreate(savedState);
+        setHasOptionsMenu(true);
+        mSettings = BrowserSettings.getInstance();
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        inflater.inflate(R.menu.autofill_profile_editor, menu);
+        mSaveMenuItem = menu.findItem(R.id.autofill_profile_editor_save_profile_menu_id);
+        updateSaveMenuItemState();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+        case R.id.autofill_profile_editor_delete_profile_menu_id:
+            // Clear the UI.
+            mFullNameEdit.setText("");
+            mEmailEdit.setText("");
+            mCompanyEdit.setText("");
+            mAddressLine1Edit.setText("");
+            mAddressLine2Edit.setText("");
+            mCityEdit.setText("");
+            mStateEdit.setText("");
+            mZipEdit.setText("");
+            mCountryEdit.setText("");
+            mPhoneEdit.setText("");
+
+            // Update browser settings and native with a null profile. This will
+            // trigger the current profile to get deleted from the DB.
+            mSettings.updateAutoFillProfile(null);
+
+            updateSaveMenuItemState();
+            return true;
+
+        case R.id.autofill_profile_editor_save_profile_menu_id:
+            AutoFillProfile newProfile = new AutoFillProfile(
+                    mSettings.getAutoFillProfileId(),
+                    mFullNameEdit.getText().toString(),
+                    mEmailEdit.getText().toString(),
+                    mCompanyEdit.getText().toString(),
+                    mAddressLine1Edit.getText().toString(),
+                    mAddressLine2Edit.getText().toString(),
+                    mCityEdit.getText().toString(),
+                    mStateEdit.getText().toString(),
+                    mZipEdit.getText().toString(),
+                    mCountryEdit.getText().toString(),
+                    mPhoneEdit.getText().toString());
+
+            mSettings.updateAutoFillProfile(newProfile);
+
+            return true;
+
+        default:
+            return false;
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View v = inflater.inflate(R.layout.autofill_settings_fragment, container, false);
+
+        mFullNameEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_name_edit);
+        mEmailEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_email_address_edit);
+        mCompanyEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_company_name_edit);
+        mAddressLine1Edit = (EditText)v.findViewById(
+                R.id.autofill_profile_editor_address_line_1_edit);
+        mAddressLine2Edit = (EditText)v.findViewById(
+                R.id.autofill_profile_editor_address_line_2_edit);
+        mCityEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_city_edit);
+        mStateEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_state_edit);
+        mZipEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_zip_code_edit);
+        mCountryEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_country_edit);
+        mPhoneEdit = (EditText)v.findViewById(R.id.autofill_profile_editor_phone_number_edit);
+
+        mFullNameEdit.addTextChangedListener(mFieldChangedListener);
+        mEmailEdit.addTextChangedListener(mFieldChangedListener);
+        mCompanyEdit.addTextChangedListener(mFieldChangedListener);
+        mAddressLine1Edit.addTextChangedListener(mFieldChangedListener);
+        mAddressLine2Edit.addTextChangedListener(mFieldChangedListener);
+        mCityEdit.addTextChangedListener(mFieldChangedListener);
+        mStateEdit.addTextChangedListener(mFieldChangedListener);
+        mZipEdit.addTextChangedListener(mFieldChangedListener);
+        mCountryEdit.addTextChangedListener(mFieldChangedListener);
+        mPhoneEdit.addTextChangedListener(new PhoneNumberValidator());
+
+        // Populate the text boxes with any pre existing AutoFill data.
+        AutoFillProfile activeProfile = mSettings.getAutoFillProfile();
+        if (activeProfile != null) {
+            mFullNameEdit.setText(activeProfile.getFullName());
+            mEmailEdit.setText(activeProfile.getEmailAddress());
+            mCompanyEdit.setText(activeProfile.getCompanyName());
+            mAddressLine1Edit.setText(activeProfile.getAddressLine1());
+            mAddressLine2Edit.setText(activeProfile.getAddressLine2());
+            mCityEdit.setText(activeProfile.getCity());
+            mStateEdit.setText(activeProfile.getState());
+            mZipEdit.setText(activeProfile.getZipCode());
+            mCountryEdit.setText(activeProfile.getCountry());
+            mPhoneEdit.setText(activeProfile.getPhoneNumber());
+        }
+
+        mInitialised = true;
+
+        updateSaveMenuItemState();
+
+        return v;
+    }
+
+    private void updateSaveMenuItemState() {
+        if (mSaveMenuItem == null) {
+            return;
+        }
+
+        if (!mInitialised) {
+            mSaveMenuItem.setEnabled(false);
+            return;
+        }
+
+        boolean currentState = mSaveMenuItem.isEnabled();
+        boolean newState = (mFullNameEdit.getText().toString().length() > 0 ||
+            mEmailEdit.getText().toString().length() > 0 ||
+            mCompanyEdit.getText().toString().length() > 0 ||
+            mAddressLine1Edit.getText().toString().length() > 0 ||
+            mAddressLine2Edit.getText().toString().length() > 0 ||
+            mCityEdit.getText().toString().length() > 0 ||
+            mStateEdit.getText().toString().length() > 0 ||
+            mZipEdit.getText().toString().length() > 0 ||
+            mCountryEdit.getText().toString().length() > 0) &&
+            mPhoneEdit.getError() == null;
+
+        if (currentState != newState) {
+            mSaveMenuItem.setEnabled(newState);
+        }
+    }
+
+    private void closeEditor() {
+        // Hide the IME if the user wants to close while an EditText has focus
+        InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(
+                Context.INPUT_METHOD_SERVICE);
+        imm.hideSoftInputFromWindow(getView().getWindowToken(), 0);
+        if (getFragmentManager().getBackStackEntryCount() > 0) {
+            getFragmentManager().popBackStack();
+        } else {
+            getActivity().finish();
+        }
+    }
+}
diff --git a/src/com/android/browser/AutofillHandler.java b/src/com/android/browser/AutofillHandler.java
new file mode 100644
index 0000000..bb392e8
--- /dev/null
+++ b/src/com/android/browser/AutofillHandler.java
@@ -0,0 +1,84 @@
+
+/*
+ * Copyright (C) 2011 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.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Message;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+
+import java.util.concurrent.CountDownLatch;
+
+import org.codeaurora.swe.AutoFillProfile;
+
+
+public class AutofillHandler {
+
+    protected AutoFillProfile mAutoFillProfile = null;
+    // Default to zero. In the case no profile is set up, the initial
+    // value will come from the AutoFillSettingsFragment when the user
+    // creates a profile. Otherwise, we'll read the ID of the last used
+    // profile from the prefs db.
+    protected String mAutoFillActiveProfileId = "";
+    private static final int NO_AUTOFILL_PROFILE_SET = 0;
+    private Context mContext;
+
+    private static final String LOGTAG = "AutofillHandler";
+
+    public AutofillHandler(Context context) {
+        mContext = context.getApplicationContext();
+        SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(mContext);
+        mAutoFillActiveProfileId = p.getString(
+                    PreferenceKeys.PREF_AUTOFILL_ACTIVE_PROFILE_ID,
+                    mAutoFillActiveProfileId);
+    }
+
+    public synchronized void setAutoFillProfile(AutoFillProfile profile) {
+        mAutoFillProfile = profile;
+        if (profile == null)
+            setActiveAutoFillProfileId("");
+        else
+            setActiveAutoFillProfileId(profile.getUniqueId());
+    }
+
+    public synchronized AutoFillProfile getAutoFillProfile() {
+        return mAutoFillProfile;
+    }
+
+    public synchronized String getAutoFillProfileId() {
+        return mAutoFillActiveProfileId;
+    }
+
+    private synchronized void setActiveAutoFillProfileId(String activeProfileId) {
+        if (mAutoFillActiveProfileId.equals(activeProfileId)) {
+            return;
+        }
+        mAutoFillActiveProfileId = activeProfileId;
+        Editor ed = PreferenceManager.
+            getDefaultSharedPreferences(mContext).edit();
+        ed.putString(PreferenceKeys.PREF_AUTOFILL_ACTIVE_PROFILE_ID, activeProfileId);
+        ed.apply();
+    }
+}
diff --git a/src/com/android/browser/AutologinBar.java b/src/com/android/browser/AutologinBar.java
new file mode 100644
index 0000000..3bbfcd9
--- /dev/null
+++ b/src/com/android/browser/AutologinBar.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.DeviceAccountLogin.AutoLoginCallback;
+
+public class AutologinBar extends LinearLayout implements OnClickListener,
+        AutoLoginCallback {
+
+    protected Spinner mAutoLoginAccount;
+    protected Button mAutoLoginLogin;
+    protected ProgressBar mAutoLoginProgress;
+    protected TextView mAutoLoginError;
+    protected View mAutoLoginCancel;
+    protected DeviceAccountLogin mAutoLoginHandler;
+    protected ArrayAdapter<String> mAccountsAdapter;
+    protected TitleBar mTitleBar;
+
+    public AutologinBar(Context context) {
+        super(context);
+    }
+
+    public AutologinBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AutologinBar(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mAutoLoginAccount = (Spinner) findViewById(R.id.autologin_account);
+        mAutoLoginLogin = (Button) findViewById(R.id.autologin_login);
+        mAutoLoginLogin.setOnClickListener(this);
+        mAutoLoginProgress = (ProgressBar) findViewById(R.id.autologin_progress);
+        mAutoLoginError = (TextView) findViewById(R.id.autologin_error);
+        mAutoLoginCancel = findViewById(R.id.autologin_close);
+        mAutoLoginCancel.setOnClickListener(this);
+    }
+
+    public void setTitleBar(TitleBar titleBar) {
+        mTitleBar = titleBar;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mAutoLoginCancel == v) {
+            if (mAutoLoginHandler != null) {
+                mAutoLoginHandler.cancel();
+                mAutoLoginHandler = null;
+            }
+            hideAutoLogin(true);
+        } else if (mAutoLoginLogin == v) {
+            if (mAutoLoginHandler != null) {
+                mAutoLoginAccount.setEnabled(false);
+                mAutoLoginLogin.setEnabled(false);
+                mAutoLoginProgress.setVisibility(View.VISIBLE);
+                mAutoLoginError.setVisibility(View.GONE);
+                mAutoLoginHandler.login(
+                        mAutoLoginAccount.getSelectedItemPosition(), this);
+            }
+        }
+    }
+
+    public void updateAutoLogin(Tab tab, boolean animate) {
+        DeviceAccountLogin login = tab.getDeviceAccountLogin();
+        if (login != null) {
+            mAutoLoginHandler = login;
+            ContextThemeWrapper wrapper = new ContextThemeWrapper(getContext(),
+                    android.R.style.Theme_Holo_Light);
+            mAccountsAdapter = new ArrayAdapter<String>(wrapper,
+                    android.R.layout.simple_spinner_item, login.getAccountNames());
+            mAccountsAdapter.setDropDownViewResource(
+                    android.R.layout.simple_spinner_dropdown_item);
+            mAutoLoginAccount.setAdapter(mAccountsAdapter);
+            mAutoLoginAccount.setSelection(0);
+            mAutoLoginAccount.setEnabled(true);
+            mAutoLoginLogin.setEnabled(true);
+            mAutoLoginProgress.setVisibility(View.INVISIBLE);
+            mAutoLoginError.setVisibility(View.GONE);
+            switch (login.getState()) {
+                case DeviceAccountLogin.PROCESSING:
+                    mAutoLoginAccount.setEnabled(false);
+                    mAutoLoginLogin.setEnabled(false);
+                    mAutoLoginProgress.setVisibility(View.VISIBLE);
+                    break;
+                case DeviceAccountLogin.FAILED:
+                    mAutoLoginProgress.setVisibility(View.INVISIBLE);
+                    mAutoLoginError.setVisibility(View.VISIBLE);
+                    break;
+                case DeviceAccountLogin.INITIAL:
+                    break;
+                default:
+                    throw new IllegalStateException();
+            }
+            showAutoLogin(animate);
+        } else {
+            hideAutoLogin(animate);
+        }
+    }
+
+    void showAutoLogin(boolean animate) {
+        mTitleBar.showAutoLogin(animate);
+    }
+
+    void hideAutoLogin(boolean animate) {
+        mTitleBar.hideAutoLogin(animate);
+    }
+
+    @Override
+    public void loginFailed() {
+        mAutoLoginAccount.setEnabled(true);
+        mAutoLoginLogin.setEnabled(true);
+        mAutoLoginProgress.setVisibility(View.INVISIBLE);
+        mAutoLoginError.setVisibility(View.VISIBLE);
+    }
+
+}
diff --git a/src/com/android/browser/BackgroundHandler.java b/src/com/android/browser/BackgroundHandler.java
new file mode 100644
index 0000000..a0d9243
--- /dev/null
+++ b/src/com/android/browser/BackgroundHandler.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2011 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.os.HandlerThread;
+import android.os.Looper;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class BackgroundHandler {
+
+    static HandlerThread sLooperThread;
+    static ExecutorService mThreadPool;
+
+    static {
+        sLooperThread = new HandlerThread("BackgroundHandler", HandlerThread.MIN_PRIORITY);
+        sLooperThread.start();
+        mThreadPool = Executors.newCachedThreadPool();
+    }
+
+    public static void execute(Runnable runnable) {
+        mThreadPool.execute(runnable);
+    }
+
+    public static Looper getLooper() {
+        return sLooperThread.getLooper();
+    }
+
+    private BackgroundHandler() {}
+}
diff --git a/src/com/android/browser/BaseUi.java b/src/com/android/browser/BaseUi.java
new file mode 100644
index 0000000..f7bd2fb
--- /dev/null
+++ b/src/com/android/browser/BaseUi.java
@@ -0,0 +1,876 @@
+/*
+ * Copyright (C) 2010 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.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.PaintDrawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.Tab.SecurityState;
+
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * UI interface definitions
+ */
+public abstract class BaseUi implements UI {
+
+    private static final String LOGTAG = "BaseUi";
+
+    protected static final FrameLayout.LayoutParams COVER_SCREEN_PARAMS =
+        new FrameLayout.LayoutParams(
+        ViewGroup.LayoutParams.MATCH_PARENT,
+        ViewGroup.LayoutParams.MATCH_PARENT);
+
+    protected static final FrameLayout.LayoutParams COVER_SCREEN_GRAVITY_CENTER =
+        new FrameLayout.LayoutParams(
+        ViewGroup.LayoutParams.MATCH_PARENT,
+        ViewGroup.LayoutParams.MATCH_PARENT,
+        Gravity.CENTER);
+
+    private static final int MSG_HIDE_TITLEBAR = 1;
+    public static final int HIDE_TITLEBAR_DELAY = 1500; // in ms
+
+    Activity mActivity;
+    UiController mUiController;
+    TabControl mTabControl;
+    protected Tab mActiveTab;
+    private InputMethodManager mInputManager;
+
+    private Drawable mLockIconSecure;
+    private Drawable mLockIconMixed;
+    protected Drawable mGenericFavicon;
+
+    protected FrameLayout mContentView;
+    protected FrameLayout mCustomViewContainer;
+    protected FrameLayout mFullscreenContainer;
+    private FrameLayout mFixedTitlebarContainer;
+
+    private View mCustomView;
+    private CustomViewCallback mCustomViewCallback;
+    private int mOriginalOrientation;
+
+    private LinearLayout mErrorConsoleContainer = null;
+
+    private UrlBarAutoShowManager mUrlBarAutoShowManager;
+
+    private Toast mStopToast;
+
+    // the default <video> poster
+    private Bitmap mDefaultVideoPoster;
+    // the video progress view
+    private View mVideoProgressView;
+
+    private boolean mActivityPaused;
+    protected boolean mUseQuickControls;
+    protected TitleBar mTitleBar;
+    private NavigationBarBase mNavigationBar;
+    protected PieControl mPieControl;
+    private boolean mBlockFocusAnimations;
+
+    public BaseUi(Activity browser, UiController controller) {
+        mActivity = browser;
+        mUiController = controller;
+        mTabControl = controller.getTabControl();
+        Resources res = mActivity.getResources();
+        mInputManager = (InputMethodManager)
+                browser.getSystemService(Activity.INPUT_METHOD_SERVICE);
+        mLockIconSecure = res.getDrawable(R.drawable.ic_secure_holo_dark);
+        mLockIconMixed = res.getDrawable(R.drawable.ic_secure_partial_holo_dark);
+        FrameLayout frameLayout = (FrameLayout) mActivity.getWindow()
+                .getDecorView().findViewById(android.R.id.content);
+        LayoutInflater.from(mActivity)
+                .inflate(R.layout.custom_screen, frameLayout);
+        mFixedTitlebarContainer = (FrameLayout) frameLayout.findViewById(
+                R.id.fixed_titlebar_container);
+        mContentView = (FrameLayout) frameLayout.findViewById(
+                R.id.main_content);
+        mCustomViewContainer = (FrameLayout) frameLayout.findViewById(
+                R.id.fullscreen_custom_content);
+        mErrorConsoleContainer = (LinearLayout) frameLayout
+                .findViewById(R.id.error_console);
+        setFullscreen(BrowserSettings.getInstance().useFullscreen());
+        mGenericFavicon = res.getDrawable(
+                R.drawable.app_web_browser_sm);
+        mTitleBar = new TitleBar(mActivity, mUiController, this,
+                mContentView);
+        mTitleBar.setProgress(100);
+        mNavigationBar = mTitleBar.getNavigationBar();
+        mUrlBarAutoShowManager = new UrlBarAutoShowManager(this);
+    }
+
+    private void cancelStopToast() {
+        if (mStopToast != null) {
+            mStopToast.cancel();
+            mStopToast = null;
+        }
+    }
+
+    // lifecycle
+
+    public void onPause() {
+        if (isCustomViewShowing()) {
+            onHideCustomView();
+        }
+        cancelStopToast();
+        mActivityPaused = true;
+    }
+
+    public void onResume() {
+        mActivityPaused = false;
+        // check if we exited without setting active tab
+        // b: 5188145
+        final Tab ct = mTabControl.getCurrentTab();
+        if (ct != null) {
+            setActiveTab(ct);
+        }
+        mTitleBar.onResume();
+    }
+
+    protected boolean isActivityPaused() {
+        return mActivityPaused;
+    }
+
+    public void onConfigurationChanged(Configuration config) {
+    }
+
+    public Activity getActivity() {
+        return mActivity;
+    }
+
+    // key handling
+
+    @Override
+    public boolean onBackKey() {
+        if (mCustomView != null) {
+            mUiController.hideCustomView();
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onMenuKey() {
+        return false;
+    }
+
+    @Override
+    public void setUseQuickControls(boolean useQuickControls) {
+        mUseQuickControls = useQuickControls;
+        mTitleBar.setUseQuickControls(mUseQuickControls);
+        if (useQuickControls) {
+            mPieControl = new PieControl(mActivity, mUiController, this);
+            mPieControl.attachToContainer(mContentView);
+        } else {
+            if (mPieControl != null) {
+                mPieControl.removeFromContainer(mContentView);
+            }
+        }
+        updateUrlBarAutoShowManagerTarget();
+    }
+
+    // Tab callbacks
+    @Override
+    public void onTabDataChanged(Tab tab) {
+        setUrlTitle(tab);
+        setFavicon(tab);
+        updateLockIconToLatest(tab);
+        updateNavigationState(tab);
+        mTitleBar.onTabDataChanged(tab);
+        mNavigationBar.onTabDataChanged(tab);
+        onProgressChanged(tab);
+    }
+
+    @Override
+    public void onProgressChanged(Tab tab) {
+        int progress = tab.getLoadProgress();
+        if (tab.inForeground()) {
+            mTitleBar.setProgress(progress);
+        }
+    }
+
+    @Override
+    public void bookmarkedStatusHasChanged(Tab tab) {
+        if (tab.inForeground()) {
+            boolean isBookmark = tab.isBookmarkedSite();
+            mNavigationBar.setCurrentUrlIsBookmark(isBookmark);
+        }
+    }
+
+    @Override
+    public void onPageStopped(Tab tab) {
+        cancelStopToast();
+        if (tab.inForeground()) {
+            mStopToast = Toast
+                    .makeText(mActivity, R.string.stopping, Toast.LENGTH_SHORT);
+            mStopToast.show();
+        }
+    }
+
+    @Override
+    public boolean needsRestoreAllTabs() {
+        return true;
+    }
+
+    @Override
+    public void addTab(Tab tab) {
+    }
+
+    @Override
+    public void setActiveTab(final Tab tab) {
+        if (tab == null) return;
+        // block unnecessary focus change animations during tab switch
+        mBlockFocusAnimations = true;
+        mHandler.removeMessages(MSG_HIDE_TITLEBAR);
+        if ((tab != mActiveTab) && (mActiveTab != null)) {
+            removeTabFromContentView(mActiveTab);
+            WebView web = mActiveTab.getWebView();
+            if (web != null) {
+                web.setOnTouchListener(null);
+            }
+        }
+        mActiveTab = tab;
+        BrowserWebView web = (BrowserWebView) mActiveTab.getWebView();
+        updateUrlBarAutoShowManagerTarget();
+        attachTabToContentView(tab);
+        if (web != null) {
+            // Request focus on the top window.
+            if (mUseQuickControls) {
+                mPieControl.forceToTop(mContentView);
+                web.setTitleBar(null);
+                mTitleBar.hide();
+            } else {
+                web.setTitleBar(mTitleBar);
+                mTitleBar.onScrollChanged();
+            }
+        }
+        mTitleBar.bringToFront();
+        tab.getTopWindow().requestFocus();
+        setShouldShowErrorConsole(tab, mUiController.shouldShowErrorConsole());
+        onTabDataChanged(tab);
+        onProgressChanged(tab);
+        mNavigationBar.setIncognitoMode(tab.isPrivateBrowsingEnabled());
+        updateAutoLogin(tab, false);
+        mBlockFocusAnimations = false;
+    }
+
+    protected void updateUrlBarAutoShowManagerTarget() {
+        WebView web = mActiveTab != null ? mActiveTab.getWebView() : null;
+        if (!mUseQuickControls && web instanceof BrowserWebView) {
+            mUrlBarAutoShowManager.setTarget((BrowserWebView) web);
+        } else {
+            mUrlBarAutoShowManager.setTarget(null);
+        }
+    }
+
+    Tab getActiveTab() {
+        return mActiveTab;
+    }
+
+    @Override
+    public void updateTabs(List<Tab> tabs) {
+    }
+
+    @Override
+    public void removeTab(Tab tab) {
+        if (mActiveTab == tab) {
+            removeTabFromContentView(tab);
+            mActiveTab = null;
+        }
+    }
+
+    @Override
+    public void detachTab(Tab tab) {
+        removeTabFromContentView(tab);
+    }
+
+    @Override
+    public void attachTab(Tab tab) {
+        attachTabToContentView(tab);
+    }
+
+    protected void attachTabToContentView(Tab tab) {
+        if ((tab == null) || (tab.getWebView() == null)) {
+            return;
+        }
+        View container = tab.getViewContainer();
+        WebView mainView  = tab.getWebView();
+        // Attach the WebView to the container and then attach the
+        // container to the content view.
+        FrameLayout wrapper =
+                (FrameLayout) container.findViewById(R.id.webview_wrapper);
+        ViewGroup parent = (ViewGroup) mainView.getParent();
+        if (parent != wrapper) {
+            if (parent != null) {
+                parent.removeView(mainView);
+            }
+            wrapper.addView(mainView);
+        }
+        parent = (ViewGroup) container.getParent();
+        if (parent != mContentView) {
+            if (parent != null) {
+                parent.removeView(container);
+            }
+            mContentView.addView(container, COVER_SCREEN_PARAMS);
+        }
+        mUiController.attachSubWindow(tab);
+    }
+
+    private void removeTabFromContentView(Tab tab) {
+        hideTitleBar();
+        // Remove the container that contains the main WebView.
+        WebView mainView = tab.getWebView();
+        View container = tab.getViewContainer();
+        if (mainView == null) {
+            return;
+        }
+        // Remove the container from the content and then remove the
+        // WebView from the container. This will trigger a focus change
+        // needed by WebView.
+        FrameLayout wrapper =
+                (FrameLayout) container.findViewById(R.id.webview_wrapper);
+        wrapper.removeView(mainView);
+        mContentView.removeView(container);
+        mUiController.endActionMode();
+        mUiController.removeSubWindow(tab);
+        ErrorConsoleView errorConsole = tab.getErrorConsole(false);
+        if (errorConsole != null) {
+            mErrorConsoleContainer.removeView(errorConsole);
+        }
+    }
+
+    @Override
+    public void onSetWebView(Tab tab, WebView webView) {
+        View container = tab.getViewContainer();
+        if (container == null) {
+            // The tab consists of a container view, which contains the main
+            // WebView, as well as any other UI elements associated with the tab.
+            container = mActivity.getLayoutInflater().inflate(R.layout.tab,
+                    mContentView, false);
+            tab.setViewContainer(container);
+        }
+        if (tab.getWebView() != webView) {
+            // Just remove the old one.
+            FrameLayout wrapper =
+                    (FrameLayout) container.findViewById(R.id.webview_wrapper);
+            wrapper.removeView(tab.getWebView());
+        }
+    }
+
+    /**
+     * create a sub window container and webview for the tab
+     * Note: this methods operates through side-effects for now
+     * it sets both the subView and subViewContainer for the given tab
+     * @param tab tab to create the sub window for
+     * @param subView webview to be set as a subwindow for the tab
+     */
+    @Override
+    public void createSubWindow(Tab tab, WebView subView) {
+        View subViewContainer = mActivity.getLayoutInflater().inflate(
+                R.layout.browser_subwindow, null);
+        ViewGroup inner = (ViewGroup) subViewContainer
+                .findViewById(R.id.inner_container);
+        inner.addView(subView, new LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.MATCH_PARENT));
+        final ImageButton cancel = (ImageButton) subViewContainer
+                .findViewById(R.id.subwindow_close);
+        final WebView cancelSubView = subView;
+        cancel.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                ((BrowserWebView) cancelSubView).getWebChromeClient().onCloseWindow(cancelSubView);
+            }
+        });
+        tab.setSubWebView(subView);
+        tab.setSubViewContainer(subViewContainer);
+    }
+
+    /**
+     * Remove the sub window from the content view.
+     */
+    @Override
+    public void removeSubWindow(View subviewContainer) {
+        mContentView.removeView(subviewContainer);
+        mUiController.endActionMode();
+    }
+
+    /**
+     * Attach the sub window to the content view.
+     */
+    @Override
+    public void attachSubWindow(View container) {
+        if (container.getParent() != null) {
+            // already attached, remove first
+            ((ViewGroup) container.getParent()).removeView(container);
+        }
+        mContentView.addView(container, COVER_SCREEN_PARAMS);
+    }
+
+    protected void refreshWebView() {
+        WebView web = getWebView();
+        if (web != null) {
+            web.invalidate();
+        }
+    }
+
+    public void editUrl(boolean clearInput, boolean forceIME) {
+        if (mUiController.isInCustomActionMode()) {
+            mUiController.endActionMode();
+        }
+        showTitleBar();
+        if ((getActiveTab() != null) && !getActiveTab().isSnapshot()) {
+            mNavigationBar.startEditingUrl(clearInput, forceIME);
+        }
+    }
+
+    boolean canShowTitleBar() {
+        return !isTitleBarShowing()
+                && !isActivityPaused()
+                && (getActiveTab() != null)
+                && (getWebView() != null)
+                && !mUiController.isInCustomActionMode();
+    }
+
+    protected void showTitleBar() {
+        mHandler.removeMessages(MSG_HIDE_TITLEBAR);
+        if (canShowTitleBar()) {
+            mTitleBar.show();
+        }
+    }
+
+    protected void hideTitleBar() {
+        if (mTitleBar.isShowing()) {
+            mTitleBar.hide();
+        }
+    }
+
+    protected boolean isTitleBarShowing() {
+        return mTitleBar.isShowing();
+    }
+
+    public boolean isEditingUrl() {
+        return mTitleBar.isEditingUrl();
+    }
+
+    public void stopEditingUrl() {
+        mTitleBar.getNavigationBar().stopEditingUrl();
+    }
+
+    public TitleBar getTitleBar() {
+        return mTitleBar;
+    }
+
+    @Override
+    public void showComboView(ComboViews startingView, Bundle extras) {
+        Intent intent = new Intent(mActivity, ComboViewActivity.class);
+        intent.putExtra(ComboViewActivity.EXTRA_INITIAL_VIEW, startingView.name());
+        intent.putExtra(ComboViewActivity.EXTRA_COMBO_ARGS, extras);
+        Tab t = getActiveTab();
+        if (t != null) {
+            intent.putExtra(ComboViewActivity.EXTRA_CURRENT_URL, t.getUrl());
+        }
+        mActivity.startActivityForResult(intent, Controller.COMBO_VIEW);
+    }
+
+    @Override
+    public void showCustomView(View view, int requestedOrientation,
+            CustomViewCallback callback) {
+        // if a view already exists then immediately terminate the new one
+        if (mCustomView != null) {
+            callback.onCustomViewHidden();
+            return;
+        }
+
+        mOriginalOrientation = mActivity.getRequestedOrientation();
+        FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
+        mFullscreenContainer = new FullscreenHolder(mActivity);
+        mFullscreenContainer.addView(view, COVER_SCREEN_PARAMS);
+        decor.addView(mFullscreenContainer, COVER_SCREEN_PARAMS);
+        mCustomView = view;
+        setFullscreen(true);
+        ((BrowserWebView) getWebView()).setVisibility(View.INVISIBLE);
+        mCustomViewCallback = callback;
+        mActivity.setRequestedOrientation(requestedOrientation);
+    }
+
+    @Override
+    public void onHideCustomView() {
+        ((BrowserWebView) getWebView()).setVisibility(View.VISIBLE);
+        if (mCustomView == null)
+            return;
+        setFullscreen(false);
+        FrameLayout decor = (FrameLayout) mActivity.getWindow().getDecorView();
+        decor.removeView(mFullscreenContainer);
+        mFullscreenContainer = null;
+        mCustomView = null;
+        mCustomViewCallback.onCustomViewHidden();
+        // Show the content view.
+        mActivity.setRequestedOrientation(mOriginalOrientation);
+    }
+
+    @Override
+    public boolean isCustomViewShowing() {
+        return mCustomView != null;
+    }
+
+    protected void dismissIME() {
+        if (mInputManager.isActive()) {
+            mInputManager.hideSoftInputFromWindow(mContentView.getWindowToken(),
+                    0);
+        }
+    }
+
+    @Override
+    public boolean isWebShowing() {
+        return mCustomView == null;
+    }
+
+    @Override
+    public void showAutoLogin(Tab tab) {
+        updateAutoLogin(tab, true);
+    }
+
+    @Override
+    public void hideAutoLogin(Tab tab) {
+        updateAutoLogin(tab, true);
+    }
+
+    // -------------------------------------------------------------------------
+
+    protected void updateNavigationState(Tab tab) {
+    }
+
+    protected void updateAutoLogin(Tab tab, boolean animate) {
+        mTitleBar.updateAutoLogin(tab, animate);
+    }
+
+    /**
+     * Update the lock icon to correspond to our latest state.
+     */
+    protected void updateLockIconToLatest(Tab t) {
+        if (t != null && t.inForeground()) {
+            updateLockIconImage(t.getSecurityState());
+        }
+    }
+
+    /**
+     * Updates the lock-icon image in the title-bar.
+     */
+    private void updateLockIconImage(SecurityState securityState) {
+        Drawable d = null;
+        if (securityState == SecurityState.SECURITY_STATE_SECURE) {
+            d = mLockIconSecure;
+        } else if (securityState == SecurityState.SECURITY_STATE_MIXED
+                || securityState == SecurityState.SECURITY_STATE_BAD_CERTIFICATE) {
+            // TODO: It would be good to have different icons for insecure vs mixed content.
+            // See http://b/5403800
+            d = mLockIconMixed;
+        }
+        mNavigationBar.setLock(d);
+    }
+
+    protected void setUrlTitle(Tab tab) {
+        String url = tab.getUrl();
+        String title = tab.getTitle();
+        if (TextUtils.isEmpty(title)) {
+            title = url;
+        }
+        if (tab.inForeground()) {
+            mNavigationBar.setDisplayTitle(url);
+        }
+    }
+
+    // Set the favicon in the title bar.
+    protected void setFavicon(Tab tab) {
+        if (tab.inForeground()) {
+            Bitmap icon = tab.getFavicon();
+            mNavigationBar.setFavicon(icon);
+        }
+    }
+
+    @Override
+    public void onActionModeFinished(boolean inLoad) {
+    }
+
+    // active tabs page
+
+    public void showActiveTabsPage() {
+    }
+
+    /**
+     * Remove the active tabs page.
+     */
+    public void removeActiveTabsPage() {
+    }
+
+    // menu handling callbacks
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        return true;
+    }
+
+    @Override
+    public void updateMenuState(Tab tab, Menu menu) {
+    }
+
+    @Override
+    public void onOptionsMenuOpened() {
+    }
+
+    @Override
+    public void onExtendedMenuOpened() {
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        return false;
+    }
+
+    @Override
+    public void onOptionsMenuClosed(boolean inLoad) {
+    }
+
+    @Override
+    public void onExtendedMenuClosed(boolean inLoad) {
+    }
+
+    @Override
+    public void onContextMenuCreated(Menu menu) {
+    }
+
+    @Override
+    public void onContextMenuClosed(Menu menu, boolean inLoad) {
+    }
+
+    // error console
+
+    @Override
+    public void setShouldShowErrorConsole(Tab tab, boolean flag) {
+        if (tab == null) return;
+        ErrorConsoleView errorConsole = tab.getErrorConsole(true);
+        if (flag) {
+            // Setting the show state of the console will cause it's the layout
+            // to be inflated.
+            if (errorConsole.numberOfErrors() > 0) {
+                errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
+            } else {
+                errorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
+            }
+            if (errorConsole.getParent() != null) {
+                mErrorConsoleContainer.removeView(errorConsole);
+            }
+            // Now we can add it to the main view.
+            mErrorConsoleContainer.addView(errorConsole,
+                    new LinearLayout.LayoutParams(
+                            ViewGroup.LayoutParams.MATCH_PARENT,
+                            ViewGroup.LayoutParams.WRAP_CONTENT));
+        } else {
+            mErrorConsoleContainer.removeView(errorConsole);
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // Helper function for WebChromeClient
+    // -------------------------------------------------------------------------
+
+    @Override
+    public Bitmap getDefaultVideoPoster() {
+        if (mDefaultVideoPoster == null) {
+            mDefaultVideoPoster = BitmapFactory.decodeResource(
+                    mActivity.getResources(), R.drawable.default_video_poster);
+        }
+        return mDefaultVideoPoster;
+    }
+
+    @Override
+    public View getVideoLoadingProgressView() {
+        if (mVideoProgressView == null) {
+            LayoutInflater inflater = LayoutInflater.from(mActivity);
+            mVideoProgressView = inflater.inflate(
+                    R.layout.video_loading_progress, null);
+        }
+        return mVideoProgressView;
+    }
+
+    @Override
+    public void showMaxTabsWarning() {
+        Toast warning = Toast.makeText(mActivity,
+                mActivity.getString(R.string.max_tabs_warning),
+                Toast.LENGTH_SHORT);
+        warning.show();
+    }
+
+    protected WebView getWebView() {
+        if (mActiveTab != null) {
+            return mActiveTab.getWebView();
+        } else {
+            return null;
+        }
+    }
+
+    public void setFullscreen(boolean enabled) {
+        Window win = mActivity.getWindow();
+        WindowManager.LayoutParams winParams = win.getAttributes();
+        final int bits = WindowManager.LayoutParams.FLAG_FULLSCREEN;
+        if (enabled) {
+            winParams.flags |=  bits;
+        } else {
+            winParams.flags &= ~bits;
+            if (mCustomView != null) {
+                mCustomView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+            } else {
+                mContentView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+            }
+        }
+        win.setAttributes(winParams);
+    }
+
+    public Drawable getFaviconDrawable(Bitmap icon) {
+        Drawable[] array = new Drawable[3];
+        array[0] = new PaintDrawable(Color.BLACK);
+        PaintDrawable p = new PaintDrawable(Color.WHITE);
+        array[1] = p;
+        if (icon == null) {
+            array[2] = mGenericFavicon;
+        } else {
+            array[2] = new BitmapDrawable(icon);
+        }
+        LayerDrawable d = new LayerDrawable(array);
+        d.setLayerInset(1, 1, 1, 1, 1);
+        d.setLayerInset(2, 2, 2, 2, 2);
+        return d;
+    }
+
+    public boolean isLoading() {
+        return mActiveTab != null ? mActiveTab.inPageLoad() : false;
+    }
+
+    /**
+     * Suggest to the UI that the title bar can be hidden. The UI will then
+     * decide whether or not to hide based off a number of factors, such
+     * as if the user is editing the URL bar or if the page is loading
+     */
+    public void suggestHideTitleBar() {
+        if (!isLoading() && !isEditingUrl() && !mTitleBar.wantsToBeVisible()
+                && !mNavigationBar.isMenuShowing()) {
+            hideTitleBar();
+        }
+    }
+
+    protected final void showTitleBarForDuration() {
+        showTitleBarForDuration(HIDE_TITLEBAR_DELAY);
+    }
+
+    protected final void showTitleBarForDuration(long duration) {
+        showTitleBar();
+        Message msg = Message.obtain(mHandler, MSG_HIDE_TITLEBAR);
+        mHandler.sendMessageDelayed(msg, duration);
+    }
+
+    protected Handler mHandler = new Handler() {
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_HIDE_TITLEBAR) {
+                suggestHideTitleBar();
+            }
+            BaseUi.this.handleMessage(msg);
+        }
+    };
+
+    protected void handleMessage(Message msg) {}
+
+    @Override
+    public void showWeb(boolean animate) {
+        mUiController.hideCustomView();
+    }
+
+    static class FullscreenHolder extends FrameLayout {
+
+        public FullscreenHolder(Context ctx) {
+            super(ctx);
+            setBackgroundColor(ctx.getResources().getColor(R.color.black));
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent evt) {
+            return true;
+        }
+
+    }
+
+    public void addFixedTitleBar(View view) {
+        mFixedTitlebarContainer.addView(view);
+    }
+
+    public void setContentViewMarginTop(int margin) {
+        LinearLayout.LayoutParams params =
+                (LinearLayout.LayoutParams) mContentView.getLayoutParams();
+        if (params.topMargin != margin) {
+            params.topMargin = margin;
+            mContentView.setLayoutParams(params);
+        }
+    }
+
+    @Override
+    public boolean blockFocusAnimations() {
+        return mBlockFocusAnimations;
+    }
+
+    @Override
+    public void onVoiceResult(String result) {
+        mNavigationBar.onVoiceResult(result);
+    }
+
+    protected UiController getUiController() {
+        return mUiController;
+    }
+}
diff --git a/src/com/android/browser/BookmarkItem.java b/src/com/android/browser/BookmarkItem.java
new file mode 100644
index 0000000..b41ee00
--- /dev/null
+++ b/src/com/android/browser/BookmarkItem.java
@@ -0,0 +1,197 @@
+/*
+ * 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 com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.HorizontalScrollView;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+/**
+ *  Custom layout for an item representing a bookmark in the browser.
+ */
+class BookmarkItem extends HorizontalScrollView {
+
+    final static int MAX_TEXTVIEW_LEN = 80;
+
+    protected TextView    mTextView;
+    protected TextView    mUrlText;
+    protected ImageView   mImageView;
+    protected String      mUrl;
+    protected String      mTitle;
+    protected boolean mEnableScrolling = false;
+
+    /**
+     *  Instantiate a bookmark item, including a default favicon.
+     *
+     *  @param context  The application context for the item.
+     */
+    BookmarkItem(Context context) {
+        super(context);
+
+        setClickable(false);
+        setEnableScrolling(false);
+        LayoutInflater factory = LayoutInflater.from(context);
+        factory.inflate(R.layout.history_item, this);
+        mTextView = (TextView) findViewById(R.id.title);
+        mUrlText = (TextView) findViewById(R.id.url);
+        mImageView = (ImageView) findViewById(R.id.favicon);
+        View star = findViewById(R.id.star);
+        star.setVisibility(View.GONE);
+    }
+
+    /**
+     *  Copy this BookmarkItem to item.
+     *  @param item BookmarkItem to receive the info from this BookmarkItem.
+     */
+    /* package */ void copyTo(BookmarkItem item) {
+        item.mTextView.setText(mTextView.getText());
+        item.mUrlText.setText(mUrlText.getText());
+        item.mImageView.setImageDrawable(mImageView.getDrawable());
+    }
+
+    /**
+     * Return the name assigned to this bookmark item.
+     */
+    /* package */ String getName() {
+        return mTitle;
+    }
+
+    /* package */ String getUrl() {
+        return mUrl;
+    }
+
+    /**
+     *  Set the favicon for this item.
+     *
+     *  @param b    The new bitmap for this item.
+     *              If it is null, will use the default.
+     */
+    /* package */ void setFavicon(Bitmap b) {
+        if (b != null) {
+            mImageView.setImageBitmap(b);
+        } else {
+            mImageView.setImageResource(R.drawable.app_web_browser_sm);
+        }
+    }
+
+    void setFaviconBackground(Drawable d) {
+        mImageView.setBackgroundDrawable(d);
+    }
+
+    /**
+     *  Set the new name for the bookmark item.
+     *
+     *  @param name The new name for the bookmark item.
+     */
+    /* package */ void setName(String name) {
+        if (name == null) {
+            return;
+        }
+
+        mTitle = name;
+
+        if (name.length() > MAX_TEXTVIEW_LEN) {
+            name = name.substring(0, MAX_TEXTVIEW_LEN);
+        }
+
+        mTextView.setText(name);
+    }
+
+    /**
+     *  Set the new url for the bookmark item.
+     *  @param url  The new url for the bookmark item.
+     */
+    /* package */ void setUrl(String url) {
+        if (url == null) {
+            return;
+        }
+
+        mUrl = url;
+
+        url = UrlUtils.stripUrl(url);
+        if (url.length() > MAX_TEXTVIEW_LEN) {
+            url = url.substring(0, MAX_TEXTVIEW_LEN);
+        }
+
+        mUrlText.setText(url);
+    }
+
+    void setEnableScrolling(boolean enable) {
+        mEnableScrolling = enable;
+        setFocusable(mEnableScrolling);
+        setFocusableInTouchMode(mEnableScrolling);
+        requestDisallowInterceptTouchEvent(!mEnableScrolling);
+        requestLayout();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mEnableScrolling) {
+            return super.onTouchEvent(ev);
+        }
+        return false;
+    }
+
+    @Override
+    protected void measureChild(View child, int parentWidthMeasureSpec,
+            int parentHeightMeasureSpec) {
+        if (mEnableScrolling) {
+            super.measureChild(child, parentWidthMeasureSpec, parentHeightMeasureSpec);
+            return;
+        }
+
+        final ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+                getPaddingLeft() + getPaddingRight(), lp.width);
+        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+                getPaddingTop() + getPaddingBottom(), lp.height);
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    protected void measureChildWithMargins(View child,
+            int parentWidthMeasureSpec, int widthUsed,
+            int parentHeightMeasureSpec, int heightUsed) {
+        if (mEnableScrolling) {
+            super.measureChildWithMargins(child, parentWidthMeasureSpec,
+                    widthUsed, parentHeightMeasureSpec, heightUsed);
+            return;
+        }
+
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+                getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+                        + widthUsed, lp.width);
+        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+                getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+                        + heightUsed, lp.height);
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+}
diff --git a/src/com/android/browser/BookmarkSearch.java b/src/com/android/browser/BookmarkSearch.java
new file mode 100644
index 0000000..4d3ca0f
--- /dev/null
+++ b/src/com/android/browser/BookmarkSearch.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2009 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.os.Bundle;
+
+/**
+ * This activity is never started from the browser. Its purpose is to provide bookmark suggestions
+ * to global search (through its searchable meta-data), and to handle the intents produced
+ * by clicking such suggestions.
+ */
+public class BookmarkSearch extends Activity {
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Intent intent = getIntent();
+        if (intent != null) {
+            String action = intent.getAction();
+            if (Intent.ACTION_VIEW.equals(action)) {
+                intent.setClass(this, BrowserActivity.class);
+                startActivity(intent);
+            }
+        }
+        finish();
+    }
+
+}
diff --git a/src/com/android/browser/BookmarkUtils.java b/src/com/android/browser/BookmarkUtils.java
new file mode 100644
index 0000000..b754a62
--- /dev/null
+++ b/src/com/android/browser/BookmarkUtils.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2010 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.ActivityManager;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.PaintDrawable;
+import android.net.Uri;
+import android.os.Message;
+import android.provider.Browser;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+public class BookmarkUtils {
+    private final static String LOGTAG = "BookmarkUtils";
+
+    // 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";
+
+    enum BookmarkIconType {
+        ICON_INSTALLABLE_WEB_APP, // Icon for an installable web app (launches WebAppRuntime).
+        ICON_HOME_SHORTCUT,        // Icon for a shortcut on the home screen (launches Browser).
+        ICON_WIDGET,
+    }
+
+    /**
+     * Creates an icon to be associated with this bookmark. If available, the apple touch icon
+     * will be used, else we draw our own depending on the type of "bookmark" being created.
+     */
+    static Bitmap createIcon(Context context, Bitmap touchIcon, Bitmap favicon,
+            BookmarkIconType type) {
+        final ActivityManager am = (ActivityManager) context
+                .getSystemService(Context.ACTIVITY_SERVICE);
+        final int iconDimension = am.getLauncherLargeIconSize();
+        final int iconDensity = am.getLauncherLargeIconDensity();
+        return createIcon(context, touchIcon, favicon, type, iconDimension, iconDensity);
+    }
+
+    static Drawable createListFaviconBackground(Context context) {
+        PaintDrawable faviconBackground = new PaintDrawable();
+        Resources res = context.getResources();
+        int padding = res.getDimensionPixelSize(R.dimen.list_favicon_padding);
+        faviconBackground.setPadding(padding, padding, padding, padding);
+        faviconBackground.getPaint().setColor(context.getResources()
+                .getColor(R.color.bookmarkListFaviconBackground));
+        faviconBackground.setCornerRadius(
+                res.getDimension(R.dimen.list_favicon_corner_radius));
+        return faviconBackground;
+    }
+
+    private static Bitmap createIcon(Context context, Bitmap touchIcon,
+            Bitmap favicon, BookmarkIconType type, int iconDimension, int iconDensity) {
+        Bitmap bm = Bitmap.createBitmap(iconDimension, iconDimension, Bitmap.Config.ARGB_8888);
+        Canvas canvas = new Canvas(bm);
+        Rect iconBounds = new Rect(0, 0, bm.getWidth(), bm.getHeight());
+
+        // Use the apple-touch-icon if available
+        if (touchIcon != null) {
+            drawTouchIconToCanvas(touchIcon, canvas, iconBounds);
+        } else {
+            // No touch icon so create our own.
+            // Set the background based on the type of shortcut (either webapp or home shortcut).
+            Bitmap icon = getIconBackground(context, type, iconDensity);
+
+            if (icon != null) {
+                // Now draw the correct icon background into our new bitmap.
+                Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+                canvas.drawBitmap(icon, null, iconBounds, p);
+            }
+
+            // If we have a favicon, overlay it in a nice rounded white box on top of the
+            // background.
+            if (favicon != null) {
+                drawFaviconToCanvas(context, favicon, canvas, iconBounds, type);
+            }
+        }
+        canvas.setBitmap(null);
+        return bm;
+    }
+
+    /**
+     * Convenience method for creating an intent that will add a shortcut to the home screen.
+     */
+    static Intent createAddToHomeIntent(Context context, String url, String title,
+            Bitmap touchIcon, Bitmap favicon) {
+        Intent i = new Intent(INSTALL_SHORTCUT);
+        Intent shortcutIntent = createShortcutIntent(url);
+        i.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
+        i.putExtra(Intent.EXTRA_SHORTCUT_NAME, title);
+        i.putExtra(Intent.EXTRA_SHORTCUT_ICON, createIcon(context, touchIcon, favicon,
+                BookmarkIconType.ICON_HOME_SHORTCUT));
+
+        // Do not allow duplicate items
+        i.putExtra("duplicate", false);
+        return i;
+    }
+
+    static Intent createShortcutIntent(String url) {
+        Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+        long urlHash = url.hashCode();
+        long uniqueId = (urlHash << 32) | shortcutIntent.hashCode();
+        shortcutIntent.putExtra(Browser.EXTRA_APPLICATION_ID, Long.toString(uniqueId));
+        return shortcutIntent;
+    }
+
+    private static Bitmap getIconBackground(Context context, BookmarkIconType type, int density) {
+        if (type == BookmarkIconType.ICON_HOME_SHORTCUT) {
+            // Want to create a shortcut icon on the homescreen, so the icon
+            // background is the red bookmark.
+            Drawable drawable = context.getResources().getDrawableForDensity(
+                    R.mipmap.ic_launcher_shortcut_browser_bookmark, density);
+            if (drawable instanceof BitmapDrawable) {
+                BitmapDrawable bd = (BitmapDrawable) drawable;
+                return bd.getBitmap();
+            }
+        } else if (type == BookmarkIconType.ICON_INSTALLABLE_WEB_APP) {
+            // Use the web browser icon as the background for the icon for an installable
+            // web app.
+            Drawable drawable = context.getResources().getDrawableForDensity(
+                    R.mipmap.ic_launcher_browser, density);
+            if (drawable instanceof BitmapDrawable) {
+                BitmapDrawable bd = (BitmapDrawable) drawable;
+                return bd.getBitmap();
+            }
+        }
+        return null;
+    }
+
+    private static void drawTouchIconToCanvas(Bitmap touchIcon, Canvas canvas, Rect iconBounds) {
+        Rect src = new Rect(0, 0, touchIcon.getWidth(), touchIcon.getHeight());
+
+        // Paint used for scaling the bitmap and drawing the rounded rect.
+        Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        paint.setFilterBitmap(true);
+        canvas.drawBitmap(touchIcon, src, iconBounds, paint);
+
+        // Construct a path from a round rect. This will allow drawing with
+        // an inverse fill so we can punch a hole using the round rect.
+        Path path = new Path();
+        path.setFillType(Path.FillType.INVERSE_WINDING);
+        RectF rect = new RectF(iconBounds);
+        rect.inset(1, 1);
+        path.addRoundRect(rect, 8f, 8f, Path.Direction.CW);
+
+        // Reuse the paint and clear the outside of the rectangle.
+        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+        canvas.drawPath(path, paint);
+    }
+
+    private static void drawFaviconToCanvas(Context context, Bitmap favicon,
+            Canvas canvas, Rect iconBounds, BookmarkIconType type) {
+        // Make a Paint for the white background rectangle and for
+        // filtering the favicon.
+        Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+        p.setStyle(Paint.Style.FILL_AND_STROKE);
+        if (type == BookmarkIconType.ICON_WIDGET) {
+            p.setColor(context.getResources()
+                    .getColor(R.color.bookmarkWidgetFaviconBackground));
+        } else {
+            p.setColor(Color.WHITE);
+        }
+
+        // Create a rectangle that is slightly wider than the favicon
+        int faviconDimension = context.getResources().getDimensionPixelSize(R.dimen.favicon_size);
+        int faviconPaddedRectDimension;
+        if (type == BookmarkIconType.ICON_WIDGET) {
+            faviconPaddedRectDimension = canvas.getWidth();
+        } else {
+            faviconPaddedRectDimension = context.getResources().getDimensionPixelSize(
+                    R.dimen.favicon_padded_size);
+        }
+        float padding = (faviconPaddedRectDimension - faviconDimension) / 2;
+        final float x = iconBounds.exactCenterX() - (faviconPaddedRectDimension / 2);
+        float y = iconBounds.exactCenterY() - (faviconPaddedRectDimension / 2);
+        if (type != BookmarkIconType.ICON_WIDGET) {
+            // Note: Subtract from the y position since the box is
+            // slightly higher than center. Use padding since it is already
+            // device independent.
+            y -= padding;
+        }
+        RectF r = new RectF(x, y, x + faviconPaddedRectDimension, y + faviconPaddedRectDimension);
+        // Draw a white rounded rectangle behind the favicon
+        canvas.drawRoundRect(r, 3, 3, p);
+
+        // Draw the favicon in the same rectangle as the rounded
+        // rectangle but inset by the padding
+        // (results in a 16x16 favicon).
+        r.inset(padding, padding);
+        canvas.drawBitmap(favicon, null, r, null);
+    }
+
+    /* package */ static Uri getBookmarksUri(Context context) {
+        return BrowserContract.Bookmarks.CONTENT_URI;
+    }
+
+    /**
+     * Show a confirmation dialog to remove a bookmark.
+     * @param id Id of the bookmark to remove
+     * @param title Title of the bookmark, to be displayed in the confirmation method.
+     * @param context Package Context for strings, dialog, ContentResolver
+     * @param msg Message to send if the bookmark is deleted.
+     */
+    static void displayRemoveBookmarkDialog( final long id, final String title,
+            final Context context, final Message msg) {
+
+        new AlertDialog.Builder(context)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(context.getString(R.string.delete_bookmark_warning,
+                        title))
+                .setPositiveButton(R.string.ok,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog, int whichButton) {
+                                if (msg != null) {
+                                    msg.sendToTarget();
+                                }
+                                Runnable runnable = new Runnable(){
+                                    @Override
+                                    public void run() {
+                                        removeBookmarkOrFolder(context, id);
+                                    }
+                                };
+                                new Thread(runnable).start();
+                            }
+                        })
+                .setNegativeButton(R.string.cancel, null)
+                .show();
+    }
+
+    /**
+     * Remove the bookmark or folder.Remove all sub folders and bookmarks under current folder.
+     * @param context Package Context for strings, dialog, ContentResolver.
+     * @param id Id of the bookmark to remove.
+     */
+    private static void removeBookmarkOrFolder(Context context, long id) {
+        Cursor cursor = context.getContentResolver().query(Bookmarks.CONTENT_URI,
+                new String[] {Bookmarks._ID},
+                Bookmarks.PARENT + "=?",
+                new String[] {Long.toString(id)},
+                null);
+
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    do {
+                        removeBookmarkOrFolder(context,
+                                cursor.getLong(cursor.getColumnIndex(Bookmarks._ID)));
+                    } while (cursor.moveToNext());
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            } finally {
+                cursor.close();
+            }
+        }
+
+        context.getContentResolver().delete(
+                ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id), null, null);
+    }
+}
diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java
new file mode 100644
index 0000000..4a49520
--- /dev/null
+++ b/src/com/android/browser/Bookmarks.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2009 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.preference.PreferenceManager;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.platformsupport.BrowserContract.Images;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebIconDatabase;
+import android.widget.Toast;
+
+import java.io.ByteArrayOutputStream;
+
+/**
+ *  This class is purely to have a common place for adding/deleting bookmarks.
+ */
+public class Bookmarks {
+    // We only want the user to be able to bookmark content that
+    // the browser can handle directly.
+    private static final String acceptableBookmarkSchemes[] = {
+            "http:",
+            "https:",
+            "about:",
+            "data:",
+            "javascript:",
+            "file:",
+            "content:"
+    };
+
+    private final static String LOGTAG = "Bookmarks";
+    /**
+     *  Add a bookmark to the database.
+     *  @param context Context of the calling Activity.  This is used to make
+     *          Toast confirming that the bookmark has been added.  If the
+     *          caller provides null, the Toast will not be shown.
+     *  @param url URL of the website to be bookmarked.
+     *  @param name Provided name for the bookmark.
+     *  @param thumbnail A thumbnail for the bookmark.
+     *  @param retainIcon Whether to retain the page's icon in the icon database.
+     *          This will usually be <code>true</code> except when bookmarks are
+     *          added by a settings restore agent.
+     *  @param parent ID of the parent folder.
+     */
+    /* package */ static void addBookmark(Context context, boolean showToast, String url,
+            String name, Bitmap thumbnail, long parent) {
+        // Want to append to the beginning of the list
+        ContentValues values = new ContentValues();
+        try {
+            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+            values.put(BrowserContract.Bookmarks.TITLE, name);
+            values.put(BrowserContract.Bookmarks.URL, url);
+            values.put(BrowserContract.Bookmarks.IS_FOLDER, 0);
+            values.put(BrowserContract.Bookmarks.THUMBNAIL,
+                    bitmapToBytes(thumbnail));
+            values.put(BrowserContract.Bookmarks.PARENT, parent);
+            context.getContentResolver().insert(BrowserContract.Bookmarks.CONTENT_URI, values);
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "addBookmark", e);
+        }
+        if (showToast) {
+            Toast.makeText(context, R.string.added_to_bookmarks,
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+
+    /**
+     *  Remove a bookmark from the database.  If the url is a visited site, it
+     *  will remain in the database, but only as a history item, and not as a
+     *  bookmarked site.
+     *  @param context Context of the calling Activity.  This is used to make
+     *          Toast confirming that the bookmark has been removed and to
+     *          lookup the correct content uri.  It must not be null.
+     *  @param cr The ContentResolver being used to remove the bookmark.
+     *  @param url URL of the website to be removed.
+     */
+    /* package */ static void removeFromBookmarks(Context context,
+            ContentResolver cr, String url, String title) {
+        Cursor cursor = null;
+        try {
+            Uri uri = BookmarkUtils.getBookmarksUri(context);
+            cursor = cr.query(uri,
+                    new String[] { BrowserContract.Bookmarks._ID },
+                    BrowserContract.Bookmarks.URL + " = ? AND " +
+                            BrowserContract.Bookmarks.TITLE + " = ?",
+                    new String[] { url, title },
+                    null);
+
+            if (!cursor.moveToFirst()) {
+                return;
+            }
+
+            // Remove from bookmarks
+            WebIconDatabase.getInstance().releaseIconForPageUrl(url);
+            uri = ContentUris.withAppendedId(BrowserContract.Bookmarks.CONTENT_URI,
+                    cursor.getLong(0));
+            cr.delete(uri, null, null);
+            if (context != null) {
+                Toast.makeText(context, R.string.removed_from_bookmarks,
+                        Toast.LENGTH_LONG).show();
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "removeFromBookmarks", e);
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+    }
+
+    private static byte[] bitmapToBytes(Bitmap bm) {
+        if (bm == null) {
+            return null;
+        }
+
+        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+        return os.toByteArray();
+    }
+
+    /* package */ static boolean urlHasAcceptableScheme(String url) {
+        if (url == null) {
+            return false;
+        }
+
+        for (int i = 0; i < acceptableBookmarkSchemes.length; i++) {
+            if (url.startsWith(acceptableBookmarkSchemes[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    static final String QUERY_BOOKMARKS_WHERE =
+            Combined.URL + " == ? OR " +
+            Combined.URL + " == ?";
+
+    public static Cursor queryCombinedForUrl(ContentResolver cr,
+            String originalUrl, String url) {
+        if (cr == null || url == null) {
+            return null;
+        }
+
+        // If originalUrl is null, just set it to url.
+        if (originalUrl == null) {
+            originalUrl = url;
+        }
+
+        // Look for both the original url and the actual url. This takes in to
+        // account redirects.
+
+        final String[] selArgs = new String[] { originalUrl, url };
+        final String[] projection = new String[] { Combined.URL };
+        return cr.query(Combined.CONTENT_URI, projection, QUERY_BOOKMARKS_WHERE, selArgs, null);
+    }
+
+    // Strip the query from the given url.
+    static String removeQuery(String url) {
+        if (url == null) {
+            return null;
+        }
+        int query = url.indexOf('?');
+        String noQuery = url;
+        if (query != -1) {
+            noQuery = url.substring(0, query);
+        }
+        return noQuery;
+    }
+
+    /**
+     * Update the bookmark's favicon. This is a convenience method for updating
+     * a bookmark favicon for the originalUrl and url of the passed in WebView.
+     * @param cr The ContentResolver to use.
+     * @param originalUrl The original url before any redirects.
+     * @param url The current url.
+     * @param favicon The favicon bitmap to write to the db.
+     */
+    /* package */ static void updateFavicon(final ContentResolver cr,
+            final String originalUrl, final String url, final Bitmap favicon) {
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... unused) {
+                final ByteArrayOutputStream os = new ByteArrayOutputStream();
+                favicon.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+                // The Images update will insert if it doesn't exist
+                ContentValues values = new ContentValues();
+                values.put(Images.FAVICON, os.toByteArray());
+                updateImages(cr, originalUrl, values);
+                updateImages(cr, url, values);
+                return null;
+            }
+
+            private void updateImages(final ContentResolver cr,
+                    final String url, ContentValues values) {
+                String iurl = removeQuery(url);
+                if (!TextUtils.isEmpty(iurl)) {
+                    values.put(Images.URL, iurl);
+                    cr.update(BrowserContract.Images.CONTENT_URI, values, null, null);
+                }
+            }
+        }.execute();
+    }
+}
diff --git a/src/com/android/browser/BookmarksLoader.java b/src/com/android/browser/BookmarksLoader.java
new file mode 100644
index 0000000..9d551e3
--- /dev/null
+++ b/src/com/android/browser/BookmarksLoader.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2010 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.Context;
+import android.content.CursorLoader;
+import android.net.Uri;
+
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+public class BookmarksLoader extends CursorLoader {
+    public static final String ARG_ACCOUNT_TYPE = "acct_type";
+    public static final String ARG_ACCOUNT_NAME = "acct_name";
+
+    public static final int COLUMN_INDEX_ID = 0;
+    public static final int COLUMN_INDEX_URL = 1;
+    public static final int COLUMN_INDEX_TITLE = 2;
+    public static final int COLUMN_INDEX_FAVICON = 3;
+    public static final int COLUMN_INDEX_THUMBNAIL = 4;
+    public static final int COLUMN_INDEX_TOUCH_ICON = 5;
+    public static final int COLUMN_INDEX_IS_FOLDER = 6;
+    public static final int COLUMN_INDEX_PARENT = 8;
+    public static final int COLUMN_INDEX_TYPE = 9;
+
+    public static final String[] PROJECTION = new String[] {
+        Bookmarks._ID, // 0
+        Bookmarks.URL, // 1
+        Bookmarks.TITLE, // 2
+        Bookmarks.FAVICON, // 3
+        Bookmarks.THUMBNAIL, // 4
+        Bookmarks.TOUCH_ICON, // 5
+        Bookmarks.IS_FOLDER, // 6
+        Bookmarks.POSITION, // 7
+        Bookmarks.PARENT, // 8
+        Bookmarks.TYPE, // 9
+    };
+
+    String mAccountType;
+    String mAccountName;
+
+    public BookmarksLoader(Context context, String accountType, String accountName) {
+        super(context, addAccount(Bookmarks.CONTENT_URI_DEFAULT_FOLDER, accountType, accountName),
+                PROJECTION, null, null, null);
+        mAccountType = accountType;
+        mAccountName = accountName;
+    }
+
+    @Override
+    public void setUri(Uri uri) {
+        super.setUri(addAccount(uri, mAccountType, mAccountName));
+    }
+
+    static Uri addAccount(Uri uri, String accountType, String accountName) {
+        return uri.buildUpon().appendQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE, accountType).
+                appendQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME, accountName).build();
+    }
+}
diff --git a/src/com/android/browser/BreadCrumbView.java b/src/com/android/browser/BreadCrumbView.java
new file mode 100644
index 0000000..f6bee4a
--- /dev/null
+++ b/src/com/android/browser/BreadCrumbView.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2010 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.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.android.browser.R;
+
+/**
+ * Simple bread crumb view
+ * Use setController to receive callbacks from user interactions
+ * Use pushView, popView, clear, and getTopData to change/access the view stack
+ */
+public class BreadCrumbView extends RelativeLayout implements OnClickListener {
+    private static final int DIVIDER_PADDING = 12; // dips
+    private static final int CRUMB_PADDING = 8; // dips
+
+    public interface Controller {
+        public void onTop(BreadCrumbView view, int level, Object data);
+    }
+
+    private ImageButton mBackButton;
+    private LinearLayout mCrumbLayout;
+    private LinearLayout mBackLayout;
+    private Controller mController;
+    private List<Crumb> mCrumbs;
+    private boolean mUseBackButton;
+    private Drawable mSeparatorDrawable;
+    private float mDividerPadding;
+    private int mMaxVisible = -1;
+    private Context mContext;
+    private int mCrumbPadding;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public BreadCrumbView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public BreadCrumbView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /**
+     * @param context
+     */
+    public BreadCrumbView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        mContext = ctx;
+        setFocusable(true);
+        setGravity(Gravity.CENTER_VERTICAL);
+        mUseBackButton = false;
+        mCrumbs = new ArrayList<Crumb>();
+        mSeparatorDrawable = ctx.getResources().getDrawable(
+                                android.R.drawable.divider_horizontal_dark);
+        float density = mContext.getResources().getDisplayMetrics().density;
+        mDividerPadding = DIVIDER_PADDING * density;
+        mCrumbPadding = (int) (CRUMB_PADDING * density);
+        addCrumbLayout();
+        addBackLayout();
+    }
+
+    public void setUseBackButton(boolean useflag) {
+        mUseBackButton = useflag;
+        updateVisible();
+    }
+
+    public void setController(Controller ctl) {
+        mController = ctl;
+    }
+
+    public int getMaxVisible() {
+        return mMaxVisible;
+    }
+
+    public void setMaxVisible(int max) {
+        mMaxVisible = max;
+        updateVisible();
+    }
+
+    public int getTopLevel() {
+        return mCrumbs.size();
+    }
+
+    public Object getTopData() {
+        Crumb c = getTopCrumb();
+        if (c != null) {
+            return c.data;
+        }
+        return null;
+    }
+
+    public int size() {
+        return mCrumbs.size();
+    }
+
+    public void clear() {
+        while (mCrumbs.size() > 1) {
+            pop(false);
+        }
+        pop(true);
+    }
+
+    public void notifyController() {
+        if (mController != null) {
+            if (mCrumbs.size() > 0) {
+                mController.onTop(this, mCrumbs.size(), getTopCrumb().data);
+            } else {
+                mController.onTop(this, 0, null);
+            }
+        }
+    }
+
+    public View pushView(String name, Object data) {
+        return pushView(name, true, data);
+    }
+
+    public View pushView(String name, boolean canGoBack, Object data) {
+        Crumb crumb = new Crumb(name, canGoBack, data);
+        pushCrumb(crumb);
+        return crumb.crumbView;
+    }
+
+    public void pushView(View view, Object data) {
+        Crumb crumb = new Crumb(view, true, data);
+        pushCrumb(crumb);
+    }
+
+    public void popView() {
+        pop(true);
+    }
+
+    private void addBackButton() {
+        mBackButton = new ImageButton(mContext);
+        mBackButton.setImageResource(R.drawable.icon_up);
+        TypedValue outValue = new TypedValue();
+        getContext().getTheme().resolveAttribute(
+                android.R.attr.selectableItemBackground, outValue, true);
+        int resid = outValue.resourceId;
+        mBackButton.setBackgroundResource(resid);
+        mBackButton.setPadding(mCrumbPadding, 0, mCrumbPadding, 0);
+        mBackButton.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.MATCH_PARENT));
+        mBackButton.setOnClickListener(this);
+        mBackButton.setContentDescription(mContext.getText(
+                R.string.accessibility_button_bookmarks_folder_up));
+        mBackLayout.addView(mBackButton);
+    }
+
+    private void addParentLabel() {
+        TextView tv = new TextView(mContext);
+        tv.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+        tv.setPadding(mCrumbPadding, 0, 0, 0);
+        tv.setGravity(Gravity.CENTER_VERTICAL);
+        tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT));
+        tv.setText("/ .../");
+        tv.setSingleLine();
+        tv.setVisibility(View.GONE);
+        mCrumbLayout.addView(tv);
+    }
+
+    private void addCrumbLayout() {
+        mCrumbLayout = new LinearLayout(mContext);
+        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        params.addRule(ALIGN_PARENT_LEFT, TRUE);
+        params.setMargins(0, 0, 4 * mCrumbPadding, 0);
+        mCrumbLayout.setLayoutParams(params);
+        mCrumbLayout.setVisibility(View.VISIBLE);
+        addParentLabel();
+        addView(mCrumbLayout);
+    }
+
+    private void addBackLayout() {
+        mBackLayout= new LinearLayout(mContext);
+        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        params.addRule(ALIGN_PARENT_RIGHT, TRUE);
+        mBackLayout.setLayoutParams(params);
+        mBackLayout.setVisibility(View.GONE);
+        addSeparator();
+        addBackButton();
+        addView(mBackLayout);
+    }
+
+    private void pushCrumb(Crumb crumb) {
+        mCrumbs.add(crumb);
+        mCrumbLayout.addView(crumb.crumbView);
+        updateVisible();
+        crumb.crumbView.setOnClickListener(this);
+    }
+
+    private void addSeparator() {
+        View sep = makeDividerView();
+        sep.setLayoutParams(makeDividerLayoutParams());
+        mBackLayout.addView(sep);
+    }
+
+    private ImageView makeDividerView() {
+        ImageView result = new ImageView(mContext);
+        result.setImageDrawable(mSeparatorDrawable);
+        result.setScaleType(ImageView.ScaleType.FIT_XY);
+        return result;
+    }
+
+    private LinearLayout.LayoutParams makeDividerLayoutParams() {
+        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
+                LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
+        return params;
+    }
+
+    private void pop(boolean notify) {
+        int n = mCrumbs.size();
+        if (n > 0) {
+            removeLastView();
+            mCrumbs.remove(n - 1);
+            if (mUseBackButton) {
+                Crumb top = getTopCrumb();
+                if (top != null && top.canGoBack) {
+                    mBackLayout.setVisibility(View.VISIBLE);
+                } else {
+                    mBackLayout.setVisibility(View.GONE);
+                }
+            }
+            updateVisible();
+            if (notify) {
+                notifyController();
+            }
+        }
+    }
+
+    private void updateVisible() {
+        // start at index 1 (0 == parent label)
+        int childIndex = 1;
+        if (mMaxVisible >= 0) {
+            int invisibleCrumbs = size() - mMaxVisible;
+            if (invisibleCrumbs > 0) {
+                int crumbIndex = 0;
+                while (crumbIndex < invisibleCrumbs) {
+                    // Set the crumb to GONE.
+                    mCrumbLayout.getChildAt(childIndex).setVisibility(View.GONE);
+                    childIndex++;
+                    // Move to the next crumb.
+                    crumbIndex++;
+                }
+            }
+            // Make sure the last is visible.
+            int childCount = mCrumbLayout.getChildCount();
+            while (childIndex < childCount) {
+                mCrumbLayout.getChildAt(childIndex).setVisibility(View.VISIBLE);
+                childIndex++;
+            }
+        } else {
+            int count = getChildCount();
+            for (int i = childIndex; i < count ; i++) {
+                getChildAt(i).setVisibility(View.VISIBLE);
+            }
+        }
+        if (mUseBackButton) {
+            boolean canGoBack = getTopCrumb() != null ? getTopCrumb().canGoBack : false;
+            mBackLayout.setVisibility(canGoBack ? View.VISIBLE : View.GONE);
+            if (canGoBack) {
+                mCrumbLayout.getChildAt(0).setVisibility(VISIBLE);
+            } else {
+                mCrumbLayout.getChildAt(0).setVisibility(GONE);
+            }
+        } else {
+            mBackLayout.setVisibility(View.GONE);
+        }
+    }
+
+    private void removeLastView() {
+        int ix = mCrumbLayout.getChildCount();
+        if (ix > 0) {
+            mCrumbLayout.removeViewAt(ix-1);
+        }
+    }
+
+    Crumb getTopCrumb() {
+        Crumb crumb = null;
+        if (mCrumbs.size() > 0) {
+            crumb = mCrumbs.get(mCrumbs.size() - 1);
+        }
+        return crumb;
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mBackButton == v) {
+            popView();
+            notifyController();
+        } else {
+            // pop until view matches crumb view
+            while (v != getTopCrumb().crumbView) {
+                pop(false);
+            }
+            notifyController();
+        }
+    }
+    @Override
+    public int getBaseline() {
+        int ix = getChildCount();
+        if (ix > 0) {
+            // If there is at least one crumb, the baseline will be its
+            // baseline.
+            return getChildAt(ix-1).getBaseline();
+        }
+        return super.getBaseline();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        int height = mSeparatorDrawable.getIntrinsicHeight();
+        if (getMeasuredHeight() < height) {
+            // This should only be an issue if there are currently no separators
+            // showing; i.e. if there is one crumb and no back button.
+            int mode = View.MeasureSpec.getMode(heightMeasureSpec);
+            switch(mode) {
+                case View.MeasureSpec.AT_MOST:
+                    if (View.MeasureSpec.getSize(heightMeasureSpec) < height) {
+                        return;
+                    }
+                    break;
+                case View.MeasureSpec.EXACTLY:
+                    return;
+                default:
+                    break;
+            }
+            setMeasuredDimension(getMeasuredWidth(), height);
+        }
+    }
+
+    class Crumb {
+
+        public View crumbView;
+        public boolean canGoBack;
+        public Object data;
+
+        public Crumb(String title, boolean backEnabled, Object tag) {
+            init(makeCrumbView(title), backEnabled, tag);
+        }
+
+        public Crumb(View view, boolean backEnabled, Object tag) {
+            init(view, backEnabled, tag);
+        }
+
+        private void init(View view, boolean back, Object tag) {
+            canGoBack = back;
+            crumbView = view;
+            data = tag;
+        }
+
+        private TextView makeCrumbView(String name) {
+            TextView tv = new TextView(mContext);
+            tv.setTextAppearance(mContext, android.R.style.TextAppearance_Medium);
+            tv.setPadding(mCrumbPadding, 0, mCrumbPadding, 0);
+            tv.setGravity(Gravity.CENTER_VERTICAL);
+            tv.setText(name);
+            tv.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+                    LayoutParams.MATCH_PARENT));
+            tv.setSingleLine();
+            tv.setEllipsize(TextUtils.TruncateAt.END);
+            return tv;
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/Browser.java b/src/com/android/browser/Browser.java
new file mode 100644
index 0000000..c9b8e7b
--- /dev/null
+++ b/src/com/android/browser/Browser.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2006 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.Application;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.util.Log;
+import android.os.Process;
+
+import org.codeaurora.swe.CookieSyncManager;
+
+public class Browser extends Application {
+
+    private final static String LOGTAG = "browser";
+
+    // Set to true to enable verbose logging.
+    final static boolean LOGV_ENABLED = false;
+
+    // Set to true to enable extra debug logging.
+    final static boolean LOGD_ENABLED = true;
+
+    @Override
+    public void onCreate() {
+        super.onCreate();
+
+        if (LOGV_ENABLED)
+            Log.v(LOGTAG, "Browser.onCreate: this=" + this);
+
+        // SWE: Avoid initializing databases for sandboxed processes.
+        // Must have INITIALIZE_DATABASE permission in AndroidManifest.xml only for browser process
+        final String INITIALIZE_DATABASE="com.android.browser.permission.INITIALIZE_DATABASE";
+        final Context context = getApplicationContext();
+        if (context.checkPermission(INITIALIZE_DATABASE,
+              Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED) {
+
+                // create CookieSyncManager with current Context
+                CookieSyncManager.createInstance(this);                
+                BrowserSettings.initialize(getApplicationContext());
+                Preloader.initialize(getApplicationContext());
+        }
+    }
+}
+
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
new file mode 100644
index 0000000..1ace9fd
--- /dev/null
+++ b/src/com/android/browser/BrowserActivity.java
@@ -0,0 +1,417 @@
+/*
+ * Copyright (C) 2006 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.app.AlertDialog;
+import android.app.KeyguardManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.PowerManager;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.Window;
+import android.webkit.JavascriptInterface;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.search.DefaultSearchEngine;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.stub.NullController;
+
+import org.chromium.content.browser.TracingIntentHandler;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+public class BrowserActivity extends Activity {
+
+    public static final String ACTION_SHOW_BOOKMARKS = "show_bookmarks";
+    public static final String ACTION_SHOW_BROWSER = "show_browser";
+    public static final String ACTION_RESTART = "--restart--";
+    private static final String ACTION_START_TRACE =
+            "org.chromium.content_shell.action.PROFILE_START";
+    private static final String ACTION_STOP_TRACE =
+            "org.chromium.content_shell.action.PROFILE_STOP";
+    private static final String EXTRA_STATE = "state";
+    public static final String EXTRA_DISABLE_URL_OVERRIDE = "disable_url_override";
+
+    private final static String LOGTAG = "browser";
+
+    private final static boolean LOGV_ENABLED = Browser.LOGV_ENABLED;
+
+    private ActivityController mController = NullController.INSTANCE;
+
+
+    private Handler mHandler = new Handler();
+
+    private UiController mUiController;
+    private Handler mHandlerEx = new Handler();
+    private Runnable runnable = new Runnable() {
+        @Override
+        public void run() {
+           if (mUiController != null) {
+               WebView current = mUiController.getCurrentWebView();
+               if (current != null) {
+                   current.postInvalidate();
+               }
+           }
+        }
+    };
+
+    private BroadcastReceiver mReceiver;
+
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        if (LOGV_ENABLED) {
+            Log.v(LOGTAG, this + " onStart, has state: "
+                    + (icicle == null ? "false" : "true"));
+        }
+        super.onCreate(icicle);
+
+        if (shouldIgnoreIntents()) {
+            finish();
+            return;
+        }
+
+        // If this was a web search request, pass it on to the default web
+        // search provider and finish this activity.
+        /*
+        SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
+        boolean result = IntentHandler.handleWebSearchIntent(this, null, getIntent());
+        if (result && (searchEngine instanceof DefaultSearchEngine)) {
+            finish();
+            return;
+        }
+        */
+        mController = createController();
+
+        Intent intent = (icicle == null) ? getIntent() : null;
+        mController.start(intent);
+    }
+
+    public static boolean isTablet(Context context) {
+        return context.getResources().getBoolean(R.bool.isTablet);
+    }
+
+    private Controller createController() {
+        Controller controller = new Controller(this);
+        boolean xlarge = isTablet(this);
+        UI ui = null;
+        if (xlarge) {
+            XLargeUi tablet = new XLargeUi(this, controller);
+            ui = tablet;
+            mUiController = tablet.getUiController();
+        } else {
+            PhoneUi phone = new PhoneUi(this, controller);
+            ui = phone;
+            mUiController = phone.getUiController();
+        }
+        controller.setUi(ui);
+        return controller;
+    }
+
+    @VisibleForTesting
+    Controller getController() {
+        return (Controller) mController;
+    }
+
+    @Override
+    protected void onNewIntent(Intent intent) {
+        if (shouldIgnoreIntents()) return;
+        if (ACTION_RESTART.equals(intent.getAction())) {
+            Bundle outState = new Bundle();
+            mController.onSaveInstanceState(outState);
+            finish();
+            getApplicationContext().startActivity(
+                    new Intent(getApplicationContext(), BrowserActivity.class)
+                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                    .putExtra(EXTRA_STATE, outState));
+            return;
+        }
+        mController.handleNewIntent(intent);
+    }
+
+    private KeyguardManager mKeyguardManager;
+    private PowerManager mPowerManager;
+    private boolean shouldIgnoreIntents() {
+        // Only process intents if the screen is on and the device is unlocked
+        // aka, if we will be user-visible
+        if (mKeyguardManager == null) {
+            mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+        }
+        if (mPowerManager == null) {
+            mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+        }
+        boolean ignore = !mPowerManager.isScreenOn();
+        ignore |= mKeyguardManager.inKeyguardRestrictedInputMode();
+        if (LOGV_ENABLED) {
+            Log.v(LOGTAG, "ignore intents: " + ignore);
+        }
+        return ignore;
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (LOGV_ENABLED) {
+            Log.v(LOGTAG, "BrowserActivity.onResume: this=" + this);
+        }
+        mController.onResume();
+        IntentFilter intentFilter = new IntentFilter(ACTION_START_TRACE);
+        intentFilter.addAction(ACTION_STOP_TRACE);
+        mReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                String action = intent.getAction();
+                String extra = intent.getStringExtra("file");
+                if (ACTION_START_TRACE.equals(action)) {
+                    if (extra.isEmpty()) {
+                        Log.e(LOGTAG, "Can not start tracing without specifing saving location");
+                    } else {
+                        TracingIntentHandler.beginTracing(extra);
+                        Log.i(LOGTAG, "start tracing");
+                    }
+                } else if (ACTION_STOP_TRACE.equals(action)) {
+                    Log.i(LOGTAG, "stop tracing");
+                    TracingIntentHandler.endTracing();
+                }
+            }
+        };
+        registerReceiver(mReceiver, intentFilter);
+    }
+
+    @Override
+    public boolean onMenuOpened(int featureId, Menu menu) {
+        if (Window.FEATURE_OPTIONS_PANEL == featureId) {
+            mController.onMenuOpened(featureId, menu);
+        }
+        return true;
+    }
+
+    @Override
+    public void onOptionsMenuClosed(Menu menu) {
+        mController.onOptionsMenuClosed(menu);
+    }
+
+    @Override
+    public void onContextMenuClosed(Menu menu) {
+        super.onContextMenuClosed(menu);
+        mController.onContextMenuClosed(menu);
+    }
+
+    /**
+     *  onSaveInstanceState(Bundle map)
+     *  onSaveInstanceState is called right before onStop(). The map contains
+     *  the saved state.
+     */
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        if (LOGV_ENABLED) {
+            Log.v(LOGTAG, "BrowserActivity.onSaveInstanceState: this=" + this);
+        }
+        mController.onSaveInstanceState(outState);
+    }
+
+    @Override
+    protected void onPause() {
+        mController.onPause();
+        super.onPause();
+    }
+
+    @Override
+    protected void onDestroy() {
+        if (LOGV_ENABLED) {
+            Log.v(LOGTAG, "BrowserActivity.onDestroy: this=" + this);
+        }
+        super.onDestroy();
+        mController.onDestroy();
+        mController = NullController.INSTANCE;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mController.onConfgurationChanged(newConfig);
+
+        //For avoiding bug CR520353 temporarily, delay 300ms to refresh WebView.
+        mHandlerEx.postDelayed(runnable, 300);
+    }
+
+    @Override
+    public void onLowMemory() {
+        super.onLowMemory();
+        mController.onLowMemory();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        super.onCreateOptionsMenu(menu);
+        return mController.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        super.onPrepareOptionsMenu(menu);
+        return mController.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (!mController.onOptionsItemSelected(item)) {
+            if (item.getItemId() == R.id.about_menu_id) {
+                final AlertDialog.Builder builder = new AlertDialog.Builder(this);
+                builder.setTitle(R.string.about);
+                builder.setCancelable(true);
+                String ua = "";
+                final WebView current = getController().getCurrentWebView();
+                if (current != null) {
+                    final WebSettings s = current.getSettings();
+                    if (s != null) {
+                        ua = s.getUserAgentString();
+                    }
+                }
+                builder.setMessage("Agent:" + ua);
+                builder.setPositiveButton(android.R.string.ok, null);
+                builder.create().show();
+            }
+            return super.onOptionsItemSelected(item);
+        }
+        return true;
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v,
+            ContextMenuInfo menuInfo) {
+        mController.onCreateContextMenu(menu, v, menuInfo);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        return mController.onContextItemSelected(item);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return mController.onKeyDown(keyCode, event) ||
+            super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+        return mController.onKeyLongPress(keyCode, event) ||
+            super.onKeyLongPress(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return mController.onKeyUp(keyCode, event) ||
+            super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public void onActionModeStarted(ActionMode mode) {
+        super.onActionModeStarted(mode);
+        mController.onActionModeStarted(mode);
+    }
+
+    @Override
+    public void onActionModeFinished(ActionMode mode) {
+        super.onActionModeFinished(mode);
+        mController.onActionModeFinished(mode);
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode,
+            Intent intent) {
+        mController.onActivityResult(requestCode, resultCode, intent);
+    }
+
+    @Override
+    public boolean onSearchRequested() {
+        return mController.onSearchRequested();
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mController.dispatchKeyEvent(event)
+                || super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+        return mController.dispatchKeyShortcutEvent(event)
+                || super.dispatchKeyShortcutEvent(event);
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return mController.dispatchTouchEvent(ev)
+                || super.dispatchTouchEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchTrackballEvent(MotionEvent ev) {
+        return mController.dispatchTrackballEvent(ev)
+                || super.dispatchTrackballEvent(ev);
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+        return mController.dispatchGenericMotionEvent(ev) ||
+                super.dispatchGenericMotionEvent(ev);
+    }
+
+    // add for carrier homepage feature
+    @JavascriptInterface
+    public void loadBookmarks() {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mController instanceof Controller) {
+                    ((Controller)mController).bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+                }
+            }
+        });
+    }
+
+    // add for carrier homepage feature
+    @JavascriptInterface
+    public void loadHistory() {
+        mHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mController instanceof Controller) {
+                    ((Controller)mController).bookmarksOrHistoryPicker(ComboViews.History);
+                }
+            }
+        });
+    }
+}
diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java
new file mode 100644
index 0000000..0f5fcd8
--- /dev/null
+++ b/src/com/android/browser/BrowserBackupAgent.java
@@ -0,0 +1,226 @@
+/*
+ * Copyright (C) 2009 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.backup.BackupAgent;
+import android.app.backup.BackupDataInput;
+import android.app.backup.BackupDataOutput;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.ParcelFileDescriptor;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+import android.util.Log;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.zip.CRC32;
+
+/**
+ * Settings backup agent for the Android browser.  Currently the only thing
+ * stored is the set of bookmarks.  It's okay if I/O exceptions are thrown
+ * out of the agent; the calling code handles it and the backup operation
+ * simply fails.
+ *
+ * @hide
+ */
+public class BrowserBackupAgent extends BackupAgent {
+    static final String TAG = "BrowserBackupAgent";
+    static final boolean DEBUG = false;
+
+    static final String BOOKMARK_KEY = "_bookmarks_";
+    /** this version num MUST be incremented if the flattened-file schema ever changes */
+    static final int BACKUP_AGENT_VERSION = 0;
+
+    /**
+     * This simply preserves the existing state as we now prefer Chrome Sync
+     * to handle bookmark backup.
+     */
+    @Override
+    public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
+            ParcelFileDescriptor newState) throws IOException {
+        long savedFileSize = -1;
+        long savedCrc = -1;
+        int savedVersion = -1;
+
+        // Extract the previous bookmark file size & CRC from the saved state
+        DataInputStream in = new DataInputStream(
+                new FileInputStream(oldState.getFileDescriptor()));
+        try {
+            savedFileSize = in.readLong();
+            savedCrc = in.readLong();
+            savedVersion = in.readInt();
+        } catch (EOFException e) {
+            // It means we had no previous state; that's fine
+            return;
+        } finally {
+            if (in != null) {
+                in.close();
+            }
+        }
+        // Write the existing state
+        writeBackupState(savedFileSize, savedCrc, newState);
+    }
+
+    /**
+     * Restore from backup -- reads in the flattened bookmark file as supplied from
+     * the backup service, parses that out, and rebuilds the bookmarks table in the
+     * browser database from it.
+     */
+    @Override
+    public void onRestore(BackupDataInput data, int appVersionCode,
+            ParcelFileDescriptor newState) throws IOException {
+        long crc = -1;
+        File tmpfile = File.createTempFile("rst", null, getFilesDir());
+        try {
+            while (data.readNextHeader()) {
+                if (BOOKMARK_KEY.equals(data.getKey())) {
+                    // Read the flattened bookmark data into a temp file
+                    crc = copyBackupToFile(data, tmpfile, data.getDataSize());
+
+                    FileInputStream infstream = new FileInputStream(tmpfile);
+                    DataInputStream in = new DataInputStream(infstream);
+
+                    try {
+                        int count = in.readInt();
+                        ArrayList<Bookmark> bookmarks = new ArrayList<Bookmark>(count);
+
+                        // Read all the bookmarks, then process later -- if we can't read
+                        // all the data successfully, we don't touch the bookmarks table
+                        for (int i = 0; i < count; i++) {
+                            Bookmark mark = new Bookmark();
+                            mark.url = in.readUTF();
+                            mark.visits = in.readInt();
+                            mark.date = in.readLong();
+                            mark.created = in.readLong();
+                            mark.title = in.readUTF();
+                            bookmarks.add(mark);
+                        }
+
+                        // Okay, we have all the bookmarks -- now see if we need to add
+                        // them to the browser's database
+                        int N = bookmarks.size();
+                        int nUnique = 0;
+                        if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks");
+                        String[] urlCol = new String[] { Bookmarks.URL };
+                        for (int i = 0; i < N; i++) {
+                            Bookmark mark = bookmarks.get(i);
+
+                            // Does this URL exist in the bookmark table?
+                            Cursor cursor = getContentResolver().query(
+                                    Bookmarks.CONTENT_URI, urlCol,
+                                    Bookmarks.URL + " == ?",
+                                    new String[] { mark.url }, null);
+                            // if not, insert it
+                            if (cursor.getCount() <= 0) {
+                                if (DEBUG) Log.v(TAG, "Did not see url: " + mark.url);
+                                addBookmark(mark);
+                                nUnique++;
+                            } else {
+                                if (DEBUG) Log.v(TAG, "Skipping extant url: " + mark.url);
+                            }
+                            cursor.close();
+                        }
+                        Log.i(TAG, "Restored " + nUnique + " of " + N + " bookmarks");
+                    } catch (IOException ioe) {
+                        Log.w(TAG, "Bad backup data; not restoring");
+                        crc = -1;
+                    } finally {
+                        if (in != null) {
+                            in.close();
+                        }
+                    }
+                }
+
+                // Last, write the state we just restored from so we can discern
+                // changes whenever we get invoked for backup in the future
+                writeBackupState(tmpfile.length(), crc, newState);
+            }
+        } finally {
+            // Whatever happens, delete the temp file
+            tmpfile.delete();
+        }
+    }
+
+    void addBookmark(Bookmark mark) {
+        ContentValues values = new ContentValues();
+        values.put(Bookmarks.TITLE, mark.title);
+        values.put(Bookmarks.URL, mark.url);
+        values.put(Bookmarks.IS_FOLDER, 0);
+        values.put(Bookmarks.DATE_CREATED, mark.created);
+        values.put(Bookmarks.DATE_MODIFIED, mark.date);
+        getContentResolver().insert(Bookmarks.CONTENT_URI, values);
+    }
+
+    static class Bookmark {
+        public String url;
+        public int visits;
+        public long date;
+        public long created;
+        public String title;
+    }
+    /*
+     * Utility functions
+     */
+
+    // Read the given file from backup to a file, calculating a CRC32 along the way
+    private long copyBackupToFile(BackupDataInput data, File file, int toRead)
+            throws IOException {
+        final int CHUNK = 8192;
+        byte[] buf = new byte[CHUNK];
+        CRC32 crc = new CRC32();
+        FileOutputStream out = new FileOutputStream(file);
+
+        try {
+            while (toRead > 0) {
+                int numRead = data.readEntityData(buf, 0, CHUNK);
+                crc.update(buf, 0, numRead);
+                out.write(buf, 0, numRead);
+                toRead -= numRead;
+            }
+        } finally {
+            if (out != null) {
+                out.close();
+            }
+        }
+        return crc.getValue();
+    }
+
+    // Write the given metrics to the new state file
+    private void writeBackupState(long fileSize, long crc, ParcelFileDescriptor stateFile)
+            throws IOException {
+        DataOutputStream out = new DataOutputStream(
+                new FileOutputStream(stateFile.getFileDescriptor()));
+        try {
+            out.writeLong(fileSize);
+            out.writeLong(crc);
+            out.writeInt(BACKUP_AGENT_VERSION);
+        } finally {
+            if (out != null) {
+                out.close();
+            }
+        }
+    }
+}
diff --git a/src/com/android/browser/BrowserBookmarksAdapter.java b/src/com/android/browser/BrowserBookmarksAdapter.java
new file mode 100644
index 0000000..3b38f1e
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksAdapter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2006 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.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.util.ThreadedCursorAdapter;
+import com.android.browser.view.BookmarkContainer;
+
+public class BrowserBookmarksAdapter extends
+        ThreadedCursorAdapter<BrowserBookmarksAdapterItem> {
+
+    LayoutInflater mInflater;
+    Context mContext;
+
+    /**
+     *  Create a new BrowserBookmarksAdapter.
+     */
+    public BrowserBookmarksAdapter(Context context) {
+        // Make sure to tell the CursorAdapter to avoid the observer and auto-requery
+        // since the Loader will do that for us.
+        super(context, null);
+        mInflater = LayoutInflater.from(context);
+        mContext = context;
+    }
+
+    @Override
+    protected long getItemId(Cursor c) {
+        return c.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+    }
+
+    @Override
+    public View newView(Context context, ViewGroup parent) {
+        return mInflater.inflate(R.layout.bookmark_thumbnail, parent, false);
+    }
+
+    @Override
+    public void bindView(View view, BrowserBookmarksAdapterItem object) {
+        BookmarkContainer container = (BookmarkContainer) view;
+        container.setIgnoreRequestLayout(true);
+        bindGridView(view, mContext, object);
+        container.setIgnoreRequestLayout(false);
+    }
+
+    CharSequence getTitle(Cursor cursor) {
+        int type = cursor.getInt(BookmarksLoader.COLUMN_INDEX_TYPE);
+        switch (type) {
+        case Bookmarks.BOOKMARK_TYPE_OTHER_FOLDER:
+            return mContext.getText(R.string.other_bookmarks);
+        }
+        return cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+    }
+
+    void bindGridView(View view, Context context, BrowserBookmarksAdapterItem item) {
+        // We need to set this to handle rotation and other configuration change
+        // events. If the padding didn't change, this is a no op.
+        int padding = context.getResources()
+                .getDimensionPixelSize(R.dimen.combo_horizontalSpacing);
+        view.setPadding(padding, view.getPaddingTop(),
+                padding, view.getPaddingBottom());
+        ImageView thumb = (ImageView) view.findViewById(R.id.thumb);
+        TextView tv = (TextView) view.findViewById(R.id.label);
+
+        tv.setText(item.title);
+        if (item.is_folder) {
+            // folder
+            thumb.setImageResource(R.drawable.thumb_bookmark_widget_folder_holo);
+            thumb.setScaleType(ScaleType.FIT_END);
+            thumb.setBackground(null);
+        } else {
+            thumb.setScaleType(ScaleType.CENTER_CROP);
+            if (item.thumbnail == null || !item.has_thumbnail) {
+                thumb.setImageResource(R.drawable.browser_thumbnail);
+            } else {
+                thumb.setImageDrawable(item.thumbnail);
+            }
+            thumb.setBackgroundResource(R.drawable.border_thumb_bookmarks_widget_holo);
+        }
+    }
+
+    @Override
+    public BrowserBookmarksAdapterItem getRowObject(Cursor c,
+            BrowserBookmarksAdapterItem item) {
+        if (item == null) {
+            item = new BrowserBookmarksAdapterItem();
+        }
+        Bitmap thumbnail = item.thumbnail != null ? item.thumbnail.getBitmap() : null;
+        thumbnail = BrowserBookmarksPage.getBitmap(c,
+                BookmarksLoader.COLUMN_INDEX_THUMBNAIL, thumbnail);
+        item.has_thumbnail = thumbnail != null;
+        if (thumbnail != null
+                && (item.thumbnail == null || item.thumbnail.getBitmap() != thumbnail)) {
+            item.thumbnail = new BitmapDrawable(mContext.getResources(), thumbnail);
+        }
+        item.is_folder = c.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0;
+        item.title = getTitle(c);
+        item.url = c.getString(BookmarksLoader.COLUMN_INDEX_URL);
+        return item;
+    }
+
+    @Override
+    public BrowserBookmarksAdapterItem getLoadingObject() {
+        BrowserBookmarksAdapterItem item = new BrowserBookmarksAdapterItem();
+        return item;
+    }
+}
diff --git a/src/com/android/browser/BrowserBookmarksAdapterItem.java b/src/com/android/browser/BrowserBookmarksAdapterItem.java
new file mode 100644
index 0000000..6b99578
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksAdapterItem.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2012 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.graphics.drawable.BitmapDrawable;
+
+public class BrowserBookmarksAdapterItem {
+    public String url;
+    public CharSequence title;
+    public BitmapDrawable thumbnail;
+    public boolean has_thumbnail;
+    public boolean is_folder;
+}
diff --git a/src/com/android/browser/BrowserBookmarksPage.java b/src/com/android/browser/BrowserBookmarksPage.java
new file mode 100644
index 0000000..a255d28
--- /dev/null
+++ b/src/com/android/browser/BrowserBookmarksPage.java
@@ -0,0 +1,698 @@
+/*
+ * Copyright (C) 2006 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.app.Fragment;
+import android.app.LoaderManager;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.provider.BrowserProvider2;
+import com.android.browser.view.BookmarkExpandableView;
+import com.android.browser.view.BookmarkExpandableView.BookmarkContextMenuInfo;
+
+import java.util.HashMap;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+interface BookmarksPageCallbacks {
+    // Return true if handled
+    boolean onBookmarkSelected(Cursor c, boolean isFolder);
+    // Return true if handled
+    boolean onOpenInNewWindow(String... urls);
+}
+
+/**
+ *  View showing the user's bookmarks in the browser.
+ */
+public class BrowserBookmarksPage extends Fragment implements View.OnCreateContextMenuListener,
+        LoaderManager.LoaderCallbacks<Cursor>, BreadCrumbView.Controller,
+        OnChildClickListener {
+
+    public static class ExtraDragState {
+        public int childPosition;
+        public int groupPosition;
+    }
+
+    static final String LOGTAG = "browser";
+
+    static final int LOADER_ACCOUNTS = 1;
+    static final int LOADER_BOOKMARKS = 100;
+
+    static final String EXTRA_DISABLE_WINDOW = "disable_new_window";
+    static final String PREF_GROUP_STATE = "bbp_group_state";
+
+    static final String ACCOUNT_TYPE = "account_type";
+    static final String ACCOUNT_NAME = "account_name";
+
+    static final long DEFAULT_FOLDER_ID = -1;
+
+    BookmarksPageCallbacks mCallbacks;
+    View mRoot;
+    BookmarkExpandableView mGrid;
+    boolean mDisableNewWindow;
+    boolean mEnableContextMenu = true;
+    View mEmptyView;
+    View mHeader;
+    HashMap<Integer, BrowserBookmarksAdapter> mBookmarkAdapters = new HashMap<Integer, BrowserBookmarksAdapter>();
+    JSONObject mState;
+    long mCurrentFolderId = BrowserProvider2.FIXED_ID_ROOT;
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        if (id == LOADER_ACCOUNTS) {
+            return new AccountsLoader(getActivity());
+        } else if (id >= LOADER_BOOKMARKS) {
+            String accountType = args.getString(ACCOUNT_TYPE);
+            String accountName = args.getString(ACCOUNT_NAME);
+            BookmarksLoader bl = new BookmarksLoader(getActivity(),
+                    accountType, accountName);
+            return bl;
+        } else {
+            throw new UnsupportedOperationException("Unknown loader id " + id);
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        if (loader.getId() == LOADER_ACCOUNTS) {
+            LoaderManager lm = getLoaderManager();
+            int id = LOADER_BOOKMARKS;
+            while (cursor.moveToNext()) {
+                String accountName = cursor.getString(0);
+                String accountType = cursor.getString(1);
+                Bundle args = new Bundle();
+                args.putString(ACCOUNT_NAME, accountName);
+                args.putString(ACCOUNT_TYPE, accountType);
+                BrowserBookmarksAdapter adapter = new BrowserBookmarksAdapter(
+                        getActivity());
+                mBookmarkAdapters.put(id, adapter);
+                boolean expand = true;
+                try {
+                    expand = mState.getBoolean(accountName != null ? accountName
+                            : BookmarkExpandableView.LOCAL_ACCOUNT_NAME);
+                } catch (JSONException e) {} // no state for accountName
+                mGrid.addAccount(accountName, adapter, expand);
+                lm.restartLoader(id, args, this);
+                id++;
+            }
+            // TODO: Figure out what a reload of these means
+            // Currently, a reload is triggered whenever bookmarks change
+            // This is less than ideal
+            // It also causes UI flickering as a new adapter is created
+            // instead of re-using an existing one when the account_name is the
+            // same.
+            // For now, this is a one-shot load
+            getLoaderManager().destroyLoader(LOADER_ACCOUNTS);
+        } else if (loader.getId() >= LOADER_BOOKMARKS) {
+            BrowserBookmarksAdapter adapter = mBookmarkAdapters.get(loader.getId());
+            adapter.changeCursor(cursor);
+            if (adapter.getCount() != 0) {
+                mCurrentFolderId = adapter.getItem(0).getLong(BookmarksLoader.COLUMN_INDEX_PARENT);
+            }
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        if (loader.getId() >= LOADER_BOOKMARKS) {
+            BrowserBookmarksAdapter adapter = mBookmarkAdapters.get(loader.getId());
+            adapter.changeCursor(null);
+        }
+    }
+
+    //add for carrier feature which adds new bookmark/folder function.
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.bookmark, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        final Activity activity = getActivity();
+        if (item.getItemId() == R.id.add_bookmark_menu_id) {
+            Intent intent = new Intent(activity, AddBookmarkPage.class);
+            intent.putExtra(BrowserContract.Bookmarks.URL, "http://");
+            intent.putExtra(BrowserContract.Bookmarks.TITLE, "");
+            intent.putExtra(BrowserContract.Bookmarks.PARENT, mCurrentFolderId);
+            activity.startActivity(intent);
+        }
+        if (item.getItemId() == R.id.new_bmfolder_menu_id) {
+            Intent intent = new Intent(activity, AddBookmarkFolder.class);
+            intent.putExtra(BrowserContract.Bookmarks.PARENT, mCurrentFolderId);
+            activity.startActivity(intent);
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        if (!(item.getMenuInfo() instanceof BookmarkContextMenuInfo)) {
+            return false;
+        }
+        BookmarkContextMenuInfo i = (BookmarkContextMenuInfo) item.getMenuInfo();
+        // If we have no menu info, we can't tell which item was selected.
+        if (i == null) {
+            return false;
+        }
+
+        if (handleContextItem(item.getItemId(), i.groupPosition, i.childPosition)) {
+            return true;
+        }
+        return super.onContextItemSelected(item);
+    }
+
+    public boolean handleContextItem(int itemId, int groupPosition,
+            int childPosition) {
+        final Activity activity = getActivity();
+        BrowserBookmarksAdapter adapter = getChildAdapter(groupPosition);
+
+        switch (itemId) {
+        case R.id.open_context_menu_id:
+            loadUrl(adapter, childPosition);
+            break;
+        case R.id.edit_context_menu_id:
+            editBookmark(adapter, childPosition);
+            break;
+        case R.id.shortcut_context_menu_id:
+            Cursor c = adapter.getItem(childPosition);
+            activity.sendBroadcast(createShortcutIntent(getActivity(), c));
+            break;
+        case R.id.delete_context_menu_id:
+            displayRemoveBookmarkDialog(adapter, childPosition);
+            break;
+        case R.id.new_window_context_menu_id:
+            openInNewWindow(adapter, childPosition);
+            break;
+        case R.id.share_link_context_menu_id: {
+            Cursor cursor = adapter.getItem(childPosition);
+            Controller.sharePage(activity,
+                    cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE),
+                    cursor.getString(BookmarksLoader.COLUMN_INDEX_URL),
+                    getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON),
+                    getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_THUMBNAIL));
+            break;
+        }
+        case R.id.copy_url_context_menu_id:
+            copy(getUrl(adapter, childPosition));
+            break;
+        case R.id.homepage_context_menu_id: {
+            BrowserSettings.getInstance().setHomePage(getUrl(adapter, childPosition));
+            Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show();
+            break;
+        }
+        // Only for the Most visited page
+        case R.id.save_to_bookmarks_menu_id: {
+            Cursor cursor = adapter.getItem(childPosition);
+            String name = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+            String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL);
+            // If the site is bookmarked, the item becomes remove from
+            // bookmarks.
+            Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(), url, name);
+            break;
+        }
+        default:
+            return false;
+        }
+        return true;
+    }
+
+    static Bitmap getBitmap(Cursor cursor, int columnIndex) {
+        return getBitmap(cursor, columnIndex, null);
+    }
+
+    static ThreadLocal<Options> sOptions = new ThreadLocal<Options>() {
+        @Override
+        protected Options initialValue() {
+            return new Options();
+        };
+    };
+    static Bitmap getBitmap(Cursor cursor, int columnIndex, Bitmap inBitmap) {
+        byte[] data = cursor.getBlob(columnIndex);
+        if (data == null) {
+            return null;
+        }
+        Options opts = sOptions.get();
+        opts.inBitmap = inBitmap;
+        opts.inSampleSize = 1;
+        opts.inScaled = false;
+        try {
+            return BitmapFactory.decodeByteArray(data, 0, data.length, opts);
+        } catch (IllegalArgumentException ex) {
+            // Failed to re-use bitmap, create a new one
+            return BitmapFactory.decodeByteArray(data, 0, data.length);
+        }
+    }
+
+    private MenuItem.OnMenuItemClickListener mContextItemClickListener =
+            new MenuItem.OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            return onContextItemSelected(item);
+        }
+    };
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+        BookmarkContextMenuInfo info = (BookmarkContextMenuInfo) menuInfo;
+        BrowserBookmarksAdapter adapter = getChildAdapter(info.groupPosition);
+        Cursor cursor = adapter.getItem(info.childPosition);
+        if (!canEdit(cursor)) {
+            return;
+        }
+        boolean isFolder
+                = cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0;
+
+        final Activity activity = getActivity();
+        MenuInflater inflater = activity.getMenuInflater();
+        inflater.inflate(R.menu.bookmarkscontext, menu);
+        if (isFolder) {
+            menu.setGroupVisible(R.id.FOLDER_CONTEXT_MENU, true);
+        } else {
+            menu.setGroupVisible(R.id.BOOKMARK_CONTEXT_MENU, true);
+            if (mDisableNewWindow) {
+                menu.findItem(R.id.new_window_context_menu_id).setVisible(false);
+            }
+        }
+        BookmarkItem header = new BookmarkItem(activity);
+        header.setEnableScrolling(true);
+        populateBookmarkItem(cursor, header, isFolder);
+        menu.setHeaderView(header);
+
+        int count = menu.size();
+        for (int i = 0; i < count; i++) {
+            menu.getItem(i).setOnMenuItemClickListener(mContextItemClickListener);
+        }
+    }
+
+    boolean canEdit(Cursor c) {
+        int type = c.getInt(BookmarksLoader.COLUMN_INDEX_TYPE);
+        return type == BrowserContract.Bookmarks.BOOKMARK_TYPE_BOOKMARK
+                || type == BrowserContract.Bookmarks.BOOKMARK_TYPE_FOLDER;
+    }
+
+    private void populateBookmarkItem(Cursor cursor, BookmarkItem item, boolean isFolder) {
+        item.setName(cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE));
+        if (isFolder) {
+            item.setUrl(null);
+            Bitmap bitmap =
+                BitmapFactory.decodeResource(getResources(), R.drawable.ic_folder_holo_dark);
+            item.setFavicon(bitmap);
+            new LookupBookmarkCount(getActivity(), item)
+                    .execute(cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID));
+        } else {
+            String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL);
+            item.setUrl(url);
+            Bitmap bitmap = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON);
+            item.setFavicon(bitmap);
+        }
+    }
+
+    /**
+     *  Create a new BrowserBookmarksPage.
+     */
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        SharedPreferences prefs = BrowserSettings.getInstance().getPreferences();
+        try {
+            mState = new JSONObject(prefs.getString(PREF_GROUP_STATE, "{}"));
+        } catch (JSONException e) {
+            // Parse failed, clear preference and start with empty state
+            prefs.edit().remove(PREF_GROUP_STATE).apply();
+            mState = new JSONObject();
+        }
+        Bundle args = getArguments();
+        mDisableNewWindow = args == null ? false : args.getBoolean(EXTRA_DISABLE_WINDOW, false);
+        setHasOptionsMenu(true);
+        if (mCallbacks == null && getActivity() instanceof CombinedBookmarksCallbacks) {
+            mCallbacks = new CombinedBookmarksCallbackWrapper(
+                    (CombinedBookmarksCallbacks) getActivity());
+        }
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        try {
+            mState = mGrid.saveGroupState();
+            // Save state
+            SharedPreferences prefs = BrowserSettings.getInstance().getPreferences();
+            prefs.edit()
+                    .putString(PREF_GROUP_STATE, mState.toString())
+                    .apply();
+        } catch (JSONException e) {
+            // Not critical, ignore
+        }
+    }
+
+    private static class CombinedBookmarksCallbackWrapper
+            implements BookmarksPageCallbacks {
+
+        private CombinedBookmarksCallbacks mCombinedCallback;
+
+        private CombinedBookmarksCallbackWrapper(CombinedBookmarksCallbacks cb) {
+            mCombinedCallback = cb;
+        }
+
+        @Override
+        public boolean onOpenInNewWindow(String... urls) {
+            mCombinedCallback.openInNewTab(urls);
+            return true;
+        }
+
+        @Override
+        public boolean onBookmarkSelected(Cursor c, boolean isFolder) {
+            if (isFolder) {
+                return false;
+            }
+            mCombinedCallback.openUrl(BrowserBookmarksPage.getUrl(c));
+            return true;
+        }
+    };
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mRoot = inflater.inflate(R.layout.bookmarks, container, false);
+        mEmptyView = mRoot.findViewById(android.R.id.empty);
+
+        mGrid = (BookmarkExpandableView) mRoot.findViewById(R.id.grid);
+        mGrid.setOnChildClickListener(this);
+        mGrid.setColumnWidthFromLayout(R.layout.bookmark_thumbnail);
+        mGrid.setBreadcrumbController(this);
+        setEnableContextMenu(mEnableContextMenu);
+
+        // Start the loaders
+        LoaderManager lm = getLoaderManager();
+        lm.restartLoader(LOADER_ACCOUNTS, null, this);
+
+        return mRoot;
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        mGrid.setBreadcrumbController(null);
+        mGrid.clearAccounts();
+        LoaderManager lm = getLoaderManager();
+        lm.destroyLoader(LOADER_ACCOUNTS);
+        for (int id : mBookmarkAdapters.keySet()) {
+            lm.destroyLoader(id);
+        }
+        mBookmarkAdapters.clear();
+    }
+
+    private BrowserBookmarksAdapter getChildAdapter(int groupPosition) {
+        return mGrid.getChildAdapter(groupPosition);
+    }
+
+    private BreadCrumbView getBreadCrumbs(int groupPosition) {
+        return mGrid.getBreadCrumbs(groupPosition);
+    }
+
+    @Override
+    public boolean onChildClick(ExpandableListView parent, View v,
+            int groupPosition, int childPosition, long id) {
+        BrowserBookmarksAdapter adapter = getChildAdapter(groupPosition);
+        Cursor cursor = adapter.getItem(childPosition);
+        boolean isFolder = cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) != 0;
+        if (mCallbacks != null &&
+                mCallbacks.onBookmarkSelected(cursor, isFolder)) {
+            return true;
+        }
+
+        if (isFolder) {
+            String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+            Uri uri = ContentUris.withAppendedId(
+                    BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER, id);
+            BreadCrumbView crumbs = getBreadCrumbs(groupPosition);
+            if (crumbs != null) {
+                // update crumbs
+                crumbs.pushView(title, uri);
+                crumbs.setVisibility(View.VISIBLE);
+                Object data = crumbs.getTopData();
+                mCurrentFolderId = (data != null ? ContentUris.parseId((Uri) data)
+                        : DEFAULT_FOLDER_ID);
+            }
+            loadFolder(groupPosition, uri);
+        }
+        return true;
+    }
+
+    /* package */ static Intent createShortcutIntent(Context context, Cursor cursor) {
+        String url = cursor.getString(BookmarksLoader.COLUMN_INDEX_URL);
+        String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+        Bitmap touchIcon = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_TOUCH_ICON);
+        Bitmap favicon = getBitmap(cursor, BookmarksLoader.COLUMN_INDEX_FAVICON);
+        return BookmarkUtils.createAddToHomeIntent(context, url, title, touchIcon, favicon);
+    }
+
+    private void loadUrl(BrowserBookmarksAdapter adapter, int position) {
+        if (mCallbacks != null && adapter != null) {
+            mCallbacks.onBookmarkSelected(adapter.getItem(position), false);
+        }
+    }
+
+    private void openInNewWindow(BrowserBookmarksAdapter adapter, int position) {
+        if (mCallbacks != null) {
+            Cursor c = adapter.getItem(position);
+            boolean isFolder = c.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) == 1;
+            if (isFolder) {
+                long id = c.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+                new OpenAllInTabsTask(id).execute();
+            } else {
+                mCallbacks.onOpenInNewWindow(BrowserBookmarksPage.getUrl(c));
+            }
+        }
+    }
+
+    class OpenAllInTabsTask extends AsyncTask<Void, Void, Cursor> {
+        long mFolderId;
+        public OpenAllInTabsTask(long id) {
+            mFolderId = id;
+        }
+
+        @Override
+        protected Cursor doInBackground(Void... params) {
+            Context c = getActivity();
+            if (c == null) return null;
+            return c.getContentResolver().query(BookmarkUtils.getBookmarksUri(c),
+                    BookmarksLoader.PROJECTION, BrowserContract.Bookmarks.PARENT + "=?",
+                    new String[] { Long.toString(mFolderId) }, null);
+        }
+
+        @Override
+        protected void onPostExecute(Cursor result) {
+            if (mCallbacks != null && result.getCount() > 0) {
+                String[] urls = new String[result.getCount()];
+                int i = 0;
+                while (result.moveToNext()) {
+                    urls[i++] = BrowserBookmarksPage.getUrl(result);
+                }
+                mCallbacks.onOpenInNewWindow(urls);
+            }
+        }
+
+    }
+
+    private void editBookmark(BrowserBookmarksAdapter adapter, int position) {
+        Intent intent = new Intent(getActivity(), AddBookmarkPage.class);
+        Cursor cursor = adapter.getItem(position);
+        Bundle item = new Bundle();
+        item.putString(BrowserContract.Bookmarks.TITLE,
+                cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE));
+        item.putString(BrowserContract.Bookmarks.URL,
+                cursor.getString(BookmarksLoader.COLUMN_INDEX_URL));
+        byte[] data = cursor.getBlob(BookmarksLoader.COLUMN_INDEX_FAVICON);
+        if (data != null) {
+            item.putParcelable(BrowserContract.Bookmarks.FAVICON,
+                    BitmapFactory.decodeByteArray(data, 0, data.length));
+        }
+        item.putLong(BrowserContract.Bookmarks._ID,
+                cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID));
+        item.putLong(BrowserContract.Bookmarks.PARENT,
+                cursor.getLong(BookmarksLoader.COLUMN_INDEX_PARENT));
+        intent.putExtra(AddBookmarkPage.EXTRA_EDIT_BOOKMARK, item);
+        intent.putExtra(AddBookmarkPage.EXTRA_IS_FOLDER,
+                cursor.getInt(BookmarksLoader.COLUMN_INDEX_IS_FOLDER) == 1);
+        startActivity(intent);
+    }
+
+    private void displayRemoveBookmarkDialog(BrowserBookmarksAdapter adapter,
+            int position) {
+        // Put up a dialog asking if the user really wants to
+        // delete the bookmark
+        Cursor cursor = adapter.getItem(position);
+        long id = cursor.getLong(BookmarksLoader.COLUMN_INDEX_ID);
+        String title = cursor.getString(BookmarksLoader.COLUMN_INDEX_TITLE);
+        Context context = getActivity();
+        BookmarkUtils.displayRemoveBookmarkDialog(id, title, context, null);
+    }
+
+    private String getUrl(BrowserBookmarksAdapter adapter, int position) {
+        return getUrl(adapter.getItem(position));
+    }
+
+    /* package */ static String getUrl(Cursor c) {
+        return c.getString(BookmarksLoader.COLUMN_INDEX_URL);
+    }
+
+    private void copy(CharSequence text) {
+        ClipboardManager cm = (ClipboardManager) getActivity().getSystemService(
+                Context.CLIPBOARD_SERVICE);
+        cm.setPrimaryClip(ClipData.newRawUri(null, Uri.parse(text.toString())));
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        Resources res = getActivity().getResources();
+        mGrid.setColumnWidthFromLayout(R.layout.bookmark_thumbnail);
+        int paddingTop = (int) res.getDimension(R.dimen.combo_paddingTop);
+        mRoot.setPadding(0, paddingTop, 0, 0);
+        getActivity().invalidateOptionsMenu();
+    }
+
+    /**
+     * BreadCrumb controller callback
+     */
+    @Override
+    public void onTop(BreadCrumbView view, int level, Object data) {
+        int groupPosition = (Integer) view.getTag(R.id.group_position);
+        Uri uri = (Uri) data;
+        if (uri == null) {
+            // top level
+            uri = BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER;
+        }
+        loadFolder(groupPosition, uri);
+        if (level <= 1) {
+            view.setVisibility(View.GONE);
+        } else {
+            view.setVisibility(View.VISIBLE);
+        }
+    }
+
+    /**
+     * @param uri
+     */
+    private void loadFolder(int groupPosition, Uri uri) {
+        LoaderManager manager = getLoaderManager();
+        // This assumes groups are ordered the same as loaders
+        BookmarksLoader loader = (BookmarksLoader) ((Loader<?>)
+                manager.getLoader(LOADER_BOOKMARKS + groupPosition));
+        loader.setUri(uri);
+        loader.forceLoad();
+    }
+
+    public void setCallbackListener(BookmarksPageCallbacks callbackListener) {
+        mCallbacks = callbackListener;
+    }
+
+    public void setEnableContextMenu(boolean enable) {
+        mEnableContextMenu = enable;
+        if (mGrid != null) {
+            if (mEnableContextMenu) {
+                registerForContextMenu(mGrid);
+            } else {
+                unregisterForContextMenu(mGrid);
+                mGrid.setLongClickable(false);
+            }
+        }
+    }
+
+    private static class LookupBookmarkCount extends AsyncTask<Long, Void, Integer> {
+        Context mContext;
+        BookmarkItem mHeader;
+
+        public LookupBookmarkCount(Context context, BookmarkItem header) {
+            mContext = context.getApplicationContext();
+            mHeader = header;
+        }
+
+        @Override
+        protected Integer doInBackground(Long... params) {
+            if (params.length != 1) {
+                throw new IllegalArgumentException("Missing folder id!");
+            }
+            Uri uri = BookmarkUtils.getBookmarksUri(mContext);
+            Cursor c = mContext.getContentResolver().query(uri,
+                    null, BrowserContract.Bookmarks.PARENT + "=?",
+                    new String[] {params[0].toString()}, null);
+            return c.getCount();
+        }
+
+        @Override
+        protected void onPostExecute(Integer result) {
+            if (result > 0) {
+                mHeader.setUrl(mContext.getString(R.string.contextheader_folder_bookmarkcount,
+                        result));
+            } else if (result == 0) {
+                mHeader.setUrl(mContext.getString(R.string.contextheader_folder_empty));
+            }
+        }
+    }
+
+    static class AccountsLoader extends CursorLoader {
+
+        static String[] ACCOUNTS_PROJECTION = new String[] {
+            Accounts.ACCOUNT_NAME,
+            Accounts.ACCOUNT_TYPE
+        };
+
+        public AccountsLoader(Context context) {
+            super(context, Accounts.CONTENT_URI
+                    .buildUpon()
+                    .appendQueryParameter(BrowserProvider2.PARAM_ALLOW_EMPTY_ACCOUNTS, "false")
+                    .build(),
+                    ACCOUNTS_PROJECTION, null, null, null);
+        }
+
+    }
+}
diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java
new file mode 100644
index 0000000..14b9e40
--- /dev/null
+++ b/src/com/android/browser/BrowserHistoryPage.java
@@ -0,0 +1,675 @@
+/*
+ * 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.app.AlertDialog;
+import android.app.Dialog;
+import android.app.Fragment;
+import android.app.FragmentBreadCrumbs;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.BaseAdapter;
+import android.widget.ExpandableListView;
+import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
+import android.widget.ExpandableListView.OnChildClickListener;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.reflect.ReflectHelper;
+
+/**
+ * Activity for displaying the browser's history, divided into
+ * days of viewing.
+ */
+public class BrowserHistoryPage extends Fragment
+        implements LoaderCallbacks<Cursor>, OnChildClickListener {
+
+    static final int LOADER_HISTORY = 1;
+    static final int LOADER_MOST_VISITED = 2;
+
+    CombinedBookmarksCallbacks mCallback;
+    HistoryAdapter mAdapter;
+    HistoryChildWrapper mChildWrapper;
+    boolean mDisableNewWindow;
+    HistoryItem mContextHeader;
+    String mMostVisitsLimit;
+    ListView mGroupList, mChildList;
+    private ViewGroup mPrefsContainer;
+    private FragmentBreadCrumbs mFragmentBreadCrumbs;
+    private ExpandableListView mHistoryList;
+
+    private View mRoot;
+
+    static interface HistoryQuery {
+        static final String[] PROJECTION = new String[] {
+                Combined._ID, // 0
+                Combined.DATE_LAST_VISITED, // 1
+                Combined.TITLE, // 2
+                Combined.URL, // 3
+                Combined.FAVICON, // 4
+                Combined.VISITS, // 5
+                Combined.IS_BOOKMARK, // 6
+        };
+
+        static final int INDEX_ID = 0;
+        static final int INDEX_DATE_LAST_VISITED = 1;
+        static final int INDEX_TITE = 2;
+        static final int INDEX_URL = 3;
+        static final int INDEX_FAVICON = 4;
+        static final int INDEX_VISITS = 5;
+        static final int INDEX_IS_BOOKMARK = 6;
+    }
+
+    private void copy(CharSequence text) {
+        ClipboardManager cm = (ClipboardManager) getActivity().getSystemService(
+                Context.CLIPBOARD_SERVICE);
+        cm.setText(text);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        Uri.Builder combinedBuilder = Combined.CONTENT_URI.buildUpon();
+
+        switch (id) {
+            case LOADER_HISTORY: {
+                String sort = Combined.DATE_LAST_VISITED + " DESC";
+                String where = Combined.VISITS + " > 0";
+                CursorLoader loader = new CursorLoader(getActivity(), combinedBuilder.build(),
+                        HistoryQuery.PROJECTION, where, null, sort);
+                return loader;
+            }
+
+            case LOADER_MOST_VISITED: {
+                Uri uri = combinedBuilder
+                        .appendQueryParameter(BrowserContract.PARAM_LIMIT, mMostVisitsLimit)
+                        .build();
+                String where = Combined.VISITS + " > 0";
+                CursorLoader loader = new CursorLoader(getActivity(), uri,
+                        HistoryQuery.PROJECTION, where, null, Combined.VISITS + " DESC");
+                return loader;
+            }
+
+            default: {
+                throw new IllegalArgumentException();
+            }
+        }
+    }
+
+    void selectGroup(int position) {
+        mGroupItemClickListener.onItemClick(null,
+                mAdapter.getGroupView(position, false, null, null),
+                position, position);
+    }
+
+    void checkIfEmpty() {
+        if (mAdapter.mMostVisited != null && mAdapter.mHistoryCursor != null) {
+            // Both cursors have loaded - check to see if we have data
+            if (mAdapter.isEmpty()) {
+                mRoot.findViewById(R.id.history).setVisibility(View.GONE);
+                mRoot.findViewById(android.R.id.empty).setVisibility(View.VISIBLE);
+            } else {
+                mRoot.findViewById(R.id.history).setVisibility(View.VISIBLE);
+                mRoot.findViewById(android.R.id.empty).setVisibility(View.GONE);
+            }
+        }
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        switch (loader.getId()) {
+            case LOADER_HISTORY: {
+                mAdapter.changeCursor(data);
+                if (!mAdapter.isEmpty() && mGroupList != null
+                        && mGroupList.getCheckedItemPosition() == ListView.INVALID_POSITION) {
+                    selectGroup(0);
+                }
+
+                checkIfEmpty();
+                break;
+            }
+
+            case LOADER_MOST_VISITED: {
+                mAdapter.changeMostVisitedCursor(data);
+
+                checkIfEmpty();
+                break;
+            }
+
+            default: {
+                throw new IllegalArgumentException();
+            }
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        setHasOptionsMenu(true);
+
+        Bundle args = getArguments();
+        mDisableNewWindow = args.getBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW, false);
+        int mvlimit = getResources().getInteger(R.integer.most_visits_limit);
+        mMostVisitsLimit = Integer.toString(mvlimit);
+        mCallback = (CombinedBookmarksCallbacks) getActivity();
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        mRoot = inflater.inflate(R.layout.history, container, false);
+        mAdapter = new HistoryAdapter(getActivity());
+        ViewStub stub = (ViewStub) mRoot.findViewById(R.id.pref_stub);
+        if (stub != null) {
+            inflateTwoPane(stub);
+        } else {
+            inflateSinglePane();
+        }
+
+        // Start the loaders
+        getLoaderManager().restartLoader(LOADER_HISTORY, null, this);
+        getLoaderManager().restartLoader(LOADER_MOST_VISITED, null, this);
+
+        return mRoot;
+    }
+
+    private void inflateSinglePane() {
+        mHistoryList = (ExpandableListView) mRoot.findViewById(R.id.history);
+        mHistoryList.setAdapter(mAdapter);
+        mHistoryList.setOnChildClickListener(this);
+        registerForContextMenu(mHistoryList);
+    }
+
+    private void inflateTwoPane(ViewStub stub) {
+        stub.setLayoutResource(R.layout.preference_list_content);
+        stub.inflate();
+        mGroupList = (ListView) mRoot.findViewById(android.R.id.list);
+        mPrefsContainer = (ViewGroup) mRoot.findViewById(R.id.prefs_frame);
+        mFragmentBreadCrumbs = (FragmentBreadCrumbs) mRoot.findViewById(android.R.id.title);
+        mFragmentBreadCrumbs.setMaxVisible(1);
+        mFragmentBreadCrumbs.setActivity(getActivity());
+        mPrefsContainer.setVisibility(View.VISIBLE);
+        mGroupList.setAdapter(new HistoryGroupWrapper(mAdapter));
+        mGroupList.setOnItemClickListener(mGroupItemClickListener);
+        mGroupList.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+        mChildWrapper = new HistoryChildWrapper(mAdapter);
+        mChildList = new ListView(getActivity());
+        mChildList.setAdapter(mChildWrapper);
+        mChildList.setOnItemClickListener(mChildItemClickListener);
+        registerForContextMenu(mChildList);
+        ViewGroup prefs = (ViewGroup) mRoot.findViewById(R.id.prefs);
+        prefs.addView(mChildList);
+    }
+
+    private OnItemClickListener mGroupItemClickListener = new OnItemClickListener() {
+        @Override
+        public void onItemClick(
+                AdapterView<?> parent, View view, int position, long id) {
+            CharSequence title = ((TextView) view).getText();
+            mFragmentBreadCrumbs.setTitle(title, title);
+            mChildWrapper.setSelectedGroup(position);
+            mGroupList.setItemChecked(position, true);
+        }
+    };
+
+    private OnItemClickListener mChildItemClickListener = new OnItemClickListener() {
+        @Override
+        public void onItemClick(
+                AdapterView<?> parent, View view, int position, long id) {
+            mCallback.openUrl(((HistoryItem) view).getUrl());
+        }
+    };
+
+    @Override
+    public boolean onChildClick(ExpandableListView parent, View view,
+            int groupPosition, int childPosition, long id) {
+        mCallback.openUrl(((HistoryItem) view).getUrl());
+        return true;
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        getLoaderManager().destroyLoader(LOADER_HISTORY);
+        getLoaderManager().destroyLoader(LOADER_MOST_VISITED);
+    }
+
+    @Override
+    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        super.onCreateOptionsMenu(menu, inflater);
+        inflater.inflate(R.menu.history, menu);
+    }
+
+    void promptToClearHistory() {
+        final ContentResolver resolver = getActivity().getContentResolver();
+        final ClearHistoryTask clear = new ClearHistoryTask(resolver);
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
+                .setMessage(R.string.pref_privacy_clear_history_dlg)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setNegativeButton(R.string.cancel, null)
+                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+                     @Override
+                     public void onClick(DialogInterface dialog, int which) {
+                         if (which == DialogInterface.BUTTON_POSITIVE) {
+                             clear.start();
+                         }
+                     }
+                });
+        final Dialog dialog = builder.create();
+        dialog.show();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == R.id.clear_history_menu_id) {
+            promptToClearHistory();
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    static class ClearHistoryTask extends Thread {
+        ContentResolver mResolver;
+
+        public ClearHistoryTask(ContentResolver resolver) {
+            mResolver = resolver;
+        }
+
+        @Override
+        public void run() {
+            Browser.clearHistory(mResolver);
+        }
+    }
+
+    View getTargetView(ContextMenuInfo menuInfo) {
+        if (menuInfo instanceof AdapterContextMenuInfo) {
+            return ((AdapterContextMenuInfo) menuInfo).targetView;
+        }
+        if (menuInfo instanceof ExpandableListContextMenuInfo) {
+            return ((ExpandableListContextMenuInfo) menuInfo).targetView;
+        }
+        return null;
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+
+        View targetView = getTargetView(menuInfo);
+        if (!(targetView instanceof HistoryItem)) {
+            return;
+        }
+        HistoryItem historyItem = (HistoryItem) targetView;
+
+        // Inflate the menu
+        Activity parent = getActivity();
+        MenuInflater inflater = parent.getMenuInflater();
+        inflater.inflate(R.menu.historycontext, menu);
+
+        // Setup the header
+        if (mContextHeader == null) {
+            mContextHeader = new HistoryItem(parent, false);
+            mContextHeader.setEnableScrolling(true);
+        } else if (mContextHeader.getParent() != null) {
+            ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
+        }
+        historyItem.copyTo(mContextHeader);
+        menu.setHeaderView(mContextHeader);
+
+        // Only show open in new tab if it was not explicitly disabled
+        if (mDisableNewWindow) {
+            menu.findItem(R.id.new_window_context_menu_id).setVisible(false);
+        }
+        // For a bookmark, provide the option to remove it from bookmarks
+        if (historyItem.isBookmark()) {
+            MenuItem item = menu.findItem(R.id.save_to_bookmarks_menu_id);
+            item.setTitle(R.string.remove_from_bookmarks);
+        }
+        // decide whether to show the share link option
+        PackageManager pm = parent.getPackageManager();
+        Intent send = new Intent(Intent.ACTION_SEND);
+        send.setType("text/plain");
+        ResolveInfo ri = pm.resolveActivity(send, PackageManager.MATCH_DEFAULT_ONLY);
+        menu.findItem(R.id.share_link_context_menu_id).setVisible(ri != null);
+
+        super.onCreateContextMenu(menu, v, menuInfo);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        ContextMenuInfo menuInfo = item.getMenuInfo();
+        if (menuInfo == null) {
+            return false;
+        }
+        View targetView = getTargetView(menuInfo);
+        if (!(targetView instanceof HistoryItem)) {
+            return false;
+        }
+        HistoryItem historyItem = (HistoryItem) targetView;
+        String url = historyItem.getUrl();
+        String title = historyItem.getName();
+        Activity activity = getActivity();
+        switch (item.getItemId()) {
+            case R.id.open_context_menu_id:
+                mCallback.openUrl(url);
+                return true;
+            case R.id.new_window_context_menu_id:
+                mCallback.openInNewTab(url);
+                return true;
+            case R.id.save_to_bookmarks_menu_id:
+                if (historyItem.isBookmark()) {
+                    Bookmarks.removeFromBookmarks(activity, activity.getContentResolver(),
+                            url, title);
+                } else {
+                    Browser.saveBookmark(activity, title, url);
+                }
+                return true;
+            case R.id.share_link_context_menu_id:
+                Object[] params  = {activity,
+                            url,
+                            activity.getText(R.string.choosertitle_sharevia).toString()};
+                Class[] type = new Class[] { android.content.Context.class,
+                                             String.class,
+                                             String.class};
+                ReflectHelper.invokeStaticMethod("android.provider.Browser","sendString",
+                    type, params);
+                return true;
+            case R.id.copy_url_context_menu_id:
+                copy(url);
+                return true;
+            case R.id.delete_context_menu_id:
+                Browser.deleteFromHistory(activity.getContentResolver(), url);
+                return true;
+            case R.id.homepage_context_menu_id:
+                BrowserSettings.getInstance().setHomePage(url);
+                Toast.makeText(activity, R.string.homepage_set, Toast.LENGTH_LONG).show();
+                return true;
+            default:
+                break;
+        }
+        return super.onContextItemSelected(item);
+    }
+
+    private static abstract class HistoryWrapper extends BaseAdapter {
+
+        protected HistoryAdapter mAdapter;
+        private DataSetObserver mObserver = new DataSetObserver() {
+            @Override
+            public void onChanged() {
+                super.onChanged();
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public void onInvalidated() {
+                super.onInvalidated();
+                notifyDataSetInvalidated();
+            }
+        };
+
+        public HistoryWrapper(HistoryAdapter adapter) {
+            mAdapter = adapter;
+            mAdapter.registerDataSetObserver(mObserver);
+        }
+
+    }
+    private static class HistoryGroupWrapper extends HistoryWrapper {
+
+        public HistoryGroupWrapper(HistoryAdapter adapter) {
+            super(adapter);
+        }
+
+        @Override
+        public int getCount() {
+            return mAdapter.getGroupCount();
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return null;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return mAdapter.getGroupView(position, false, convertView, parent);
+        }
+
+    }
+
+    private static class HistoryChildWrapper extends HistoryWrapper {
+
+        private int mSelectedGroup;
+
+        public HistoryChildWrapper(HistoryAdapter adapter) {
+            super(adapter);
+        }
+
+        void setSelectedGroup(int groupPosition) {
+            mSelectedGroup = groupPosition;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return mAdapter.getChildrenCount(mSelectedGroup);
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return null;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return mAdapter.getChildView(mSelectedGroup, position,
+                    false, convertView, parent);
+        }
+
+    }
+
+    private class HistoryAdapter extends DateSortedExpandableListAdapter {
+
+        private Cursor mMostVisited, mHistoryCursor;
+        Drawable mFaviconBackground;
+
+        HistoryAdapter(Context context) {
+            super(context, HistoryQuery.INDEX_DATE_LAST_VISITED);
+            mFaviconBackground = BookmarkUtils.createListFaviconBackground(context);
+        }
+
+        @Override
+        public void changeCursor(Cursor cursor) {
+            mHistoryCursor = cursor;
+            super.changeCursor(cursor);
+        }
+
+        void changeMostVisitedCursor(Cursor cursor) {
+            if (mMostVisited == cursor) {
+                return;
+            }
+            if (mMostVisited != null) {
+                mMostVisited.unregisterDataSetObserver(mDataSetObserver);
+                mMostVisited.close();
+            }
+            mMostVisited = cursor;
+            if (mMostVisited != null) {
+                mMostVisited.registerDataSetObserver(mDataSetObserver);
+            }
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public long getChildId(int groupPosition, int childPosition) {
+            if (moveCursorToChildPosition(groupPosition, childPosition)) {
+                Cursor cursor = getCursor(groupPosition);
+                return cursor.getLong(HistoryQuery.INDEX_ID);
+            }
+            return 0;
+        }
+
+        @Override
+        public int getGroupCount() {
+            return super.getGroupCount() + (!isMostVisitedEmpty() ? 1 : 0);
+        }
+
+        @Override
+        public int getChildrenCount(int groupPosition) {
+            if (groupPosition >= super.getGroupCount()) {
+                if (isMostVisitedEmpty()) {
+                    return 0;
+                }
+                return mMostVisited.getCount();
+            }
+            return super.getChildrenCount(groupPosition);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            if (!super.isEmpty()) {
+                return false;
+            }
+            return isMostVisitedEmpty();
+        }
+
+        private boolean isMostVisitedEmpty() {
+            return mMostVisited == null
+                    || mMostVisited.isClosed()
+                    || mMostVisited.getCount() == 0;
+        }
+
+        Cursor getCursor(int groupPosition) {
+            if (groupPosition >= super.getGroupCount()) {
+                return mMostVisited;
+            }
+            return mHistoryCursor;
+        }
+
+        @Override
+        public View getGroupView(int groupPosition, boolean isExpanded,
+                View convertView, ViewGroup parent) {
+            if (groupPosition >= super.getGroupCount()) {
+                if (mMostVisited == null || mMostVisited.isClosed()) {
+                    throw new IllegalStateException("Data is not valid");
+                }
+                TextView item;
+                if (null == convertView || !(convertView instanceof TextView)) {
+                    LayoutInflater factory = LayoutInflater.from(getContext());
+                    item = (TextView) factory.inflate(R.layout.history_header, null);
+                } else {
+                    item = (TextView) convertView;
+                }
+                item.setText(R.string.tab_most_visited);
+                return item;
+            }
+            return super.getGroupView(groupPosition, isExpanded, convertView, parent);
+        }
+
+        @Override
+        boolean moveCursorToChildPosition(
+                int groupPosition, int childPosition) {
+            if (groupPosition >= super.getGroupCount()) {
+                if (mMostVisited != null && !mMostVisited.isClosed()) {
+                    mMostVisited.moveToPosition(childPosition);
+                    return true;
+                }
+                return false;
+            }
+            return super.moveCursorToChildPosition(groupPosition, childPosition);
+        }
+
+        @Override
+        public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+                View convertView, ViewGroup parent) {
+            HistoryItem item;
+            if (null == convertView || !(convertView instanceof HistoryItem)) {
+                item = new HistoryItem(getContext());
+                // Add padding on the left so it will be indented from the
+                // arrows on the group views.
+                item.setPadding(item.getPaddingLeft() + 10,
+                        item.getPaddingTop(),
+                        item.getPaddingRight(),
+                        item.getPaddingBottom());
+                item.setFaviconBackground(mFaviconBackground);
+            } else {
+                item = (HistoryItem) convertView;
+            }
+
+            // Bail early if the Cursor is closed.
+            if (!moveCursorToChildPosition(groupPosition, childPosition)) {
+                return item;
+            }
+
+            Cursor cursor = getCursor(groupPosition);
+            item.setName(cursor.getString(HistoryQuery.INDEX_TITE));
+            String url = cursor.getString(HistoryQuery.INDEX_URL);
+            item.setUrl(url);
+            byte[] data = cursor.getBlob(HistoryQuery.INDEX_FAVICON);
+            if (data != null) {
+                item.setFavicon(BitmapFactory.decodeByteArray(data, 0,
+                        data.length));
+            }
+            item.setIsBookmark(cursor.getInt(HistoryQuery.INDEX_IS_BOOKMARK) == 1);
+            return item;
+        }
+    }
+}
diff --git a/src/com/android/browser/BrowserPreferencesPage.java b/src/com/android/browser/BrowserPreferencesPage.java
new file mode 100644
index 0000000..ebc08a4
--- /dev/null
+++ b/src/com/android/browser/BrowserPreferencesPage.java
@@ -0,0 +1,102 @@
+/*
+ * 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.ActionBar;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+import android.view.MenuItem;
+
+import com.android.browser.R;
+import com.android.browser.preferences.BandwidthPreferencesFragment;
+import com.android.browser.preferences.DebugPreferencesFragment;
+
+import java.util.List;
+
+public class BrowserPreferencesPage extends PreferenceActivity {
+
+    public static final String CURRENT_PAGE = "currentPage";
+    private List<Header> mHeaders;
+
+    @Override
+    public void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+
+        ActionBar actionBar = getActionBar();
+        if (actionBar != null) {
+            actionBar.setDisplayOptions(
+                    ActionBar.DISPLAY_HOME_AS_UP, ActionBar.DISPLAY_HOME_AS_UP);
+        }
+    }
+
+    /**
+     * Populate the activity with the top-level headers.
+     */
+    @Override
+    public void onBuildHeaders(List<Header> target) {
+        loadHeadersFromResource(R.xml.preference_headers, target);
+
+        if (BrowserSettings.getInstance().isDebugEnabled()) {
+            Header debug = new Header();
+            debug.title = getText(R.string.pref_development_title);
+            debug.fragment = DebugPreferencesFragment.class.getName();
+            target.add(debug);
+        }
+        mHeaders = target;
+    }
+
+    @Override
+    public Header onGetInitialHeader() {
+        String action = getIntent().getAction();
+        if (Intent.ACTION_MANAGE_NETWORK_USAGE.equals(action)) {
+            String fragName = BandwidthPreferencesFragment.class.getName();
+            for (Header h : mHeaders) {
+                if (fragName.equals(h.fragment)) {
+                    return h;
+                }
+            }
+        }
+        return super.onGetInitialHeader();
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        switch (item.getItemId()) {
+            case android.R.id.home:
+                if (getFragmentManager().getBackStackEntryCount() > 0) {
+                    getFragmentManager().popBackStack();
+                } else {
+                    finish();
+                }
+                return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public Intent onBuildStartFragmentIntent(String fragmentName, Bundle args,
+            int titleRes, int shortTitleRes) {
+        Intent intent = super.onBuildStartFragmentIntent(fragmentName, args,
+                titleRes, shortTitleRes);
+        String url = getIntent().getStringExtra(CURRENT_PAGE);
+        intent.putExtra(CURRENT_PAGE, url);
+        return intent;
+    }
+
+}
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
new file mode 100644
index 0000000..90dcc4f
--- /dev/null
+++ b/src/com/android/browser/BrowserSettings.java
@@ -0,0 +1,1140 @@
+/*
+ * Copyright (C) 2011 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.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.content.res.AssetManager;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Build;
+import android.preference.PreferenceManager;
+import android.provider.Browser;
+import android.provider.Settings;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.webkit.WebIconDatabase;
+import android.webkit.WebStorage;
+import android.webkit.WebViewDatabase;
+
+import com.android.browser.R;
+import com.android.browser.homepages.HomeProvider;
+import com.android.browser.provider.BrowserProvider;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.search.SearchEngines;
+
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.Locale;
+import java.util.WeakHashMap;
+
+import org.codeaurora.swe.AutoFillProfile;
+import org.codeaurora.swe.CookieManager;
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebSettings.LayoutAlgorithm;
+import org.codeaurora.swe.WebSettings.PluginState;
+import org.codeaurora.swe.WebSettings.TextSize;
+import org.codeaurora.swe.WebSettings.ZoomDensity;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+/**
+ * Class for managing settings
+ */
+public class BrowserSettings implements OnSharedPreferenceChangeListener,
+        PreferenceKeys {
+
+    // TODO: Do something with this UserAgent stuff
+    private static final String DESKTOP_USERAGENT = "Mozilla/5.0 (X11; " +
+        "Linux x86_64) AppleWebKit/534.24 (KHTML, like Gecko) " +
+        "Chrome/11.0.696.34 Safari/534.24";
+
+    private static final String IPHONE_USERAGENT = "Mozilla/5.0 (iPhone; U; " +
+        "CPU iPhone OS 4_0 like Mac OS X; en-us) AppleWebKit/532.9 " +
+        "(KHTML, like Gecko) Version/4.0.5 Mobile/8A293 Safari/6531.22.7";
+
+    private static final String IPAD_USERAGENT = "Mozilla/5.0 (iPad; U; " +
+        "CPU OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.10 " +
+        "(KHTML, like Gecko) Version/4.0.4 Mobile/7B367 Safari/531.21.10";
+
+    private static final String FROYO_USERAGENT = "Mozilla/5.0 (Linux; U; " +
+        "Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 " +
+        "(KHTML, like Gecko) Version/4.0 Mobile Safari/533.1";
+
+    private static final String HONEYCOMB_USERAGENT = "Mozilla/5.0 (Linux; U; " +
+        "Android 3.1; en-us; Xoom Build/HMJ25) AppleWebKit/534.13 " +
+        "(KHTML, like Gecko) Version/4.0 Safari/534.13";
+
+    private static final String USER_AGENTS[] = { null,
+            DESKTOP_USERAGENT,
+            IPHONE_USERAGENT,
+            IPAD_USERAGENT,
+            FROYO_USERAGENT,
+            HONEYCOMB_USERAGENT,
+    };
+
+    private static final String TAG = "BrowserSettings";
+    // The minimum min font size
+    // Aka, the lower bounds for the min font size range
+    // which is 1:5..24
+    private static final int MIN_FONT_SIZE_OFFSET = 5;
+    // The initial value in the text zoom range
+    // This is what represents 100% in the SeekBarPreference range
+    private static final int TEXT_ZOOM_START_VAL = 10;
+    // The size of a single step in the text zoom range, in percent
+    private static final int TEXT_ZOOM_STEP = 5;
+    // The initial value in the double tap zoom range
+    // This is what represents 100% in the SeekBarPreference range
+    private static final int DOUBLE_TAP_ZOOM_START_VAL = 5;
+    // The size of a single step in the double tap zoom range, in percent
+    private static final int DOUBLE_TAP_ZOOM_STEP = 5;
+
+    private static BrowserSettings sInstance;
+
+    private Context mContext;
+    private SharedPreferences mPrefs;
+    private LinkedList<WeakReference<WebSettings>> mManagedSettings;
+    private Controller mController;
+    private WebStorageSizeManager mWebStorageSizeManager;
+    private AutofillHandler mAutofillHandler;
+    private WeakHashMap<WebSettings, String> mCustomUserAgents;
+    private static boolean sInitialized = false;
+    private boolean mNeedsSharedSync = true;
+    private float mFontSizeMult = 1.0f;
+
+    // Current state of network-dependent settings
+    private boolean mLinkPrefetchAllowed = true;
+
+    // Cached values
+    private int mPageCacheCapacity = 1;
+    private String mAppCachePath;
+
+    // Cached settings
+    private SearchEngine mSearchEngine;
+
+    private static String sFactoryResetUrl;
+
+    // add for carrier feature
+    private static Context sResPackageCtx;
+    private android.os.CountDownTimer mCountDownTimer;
+
+    //Determine if WebView is Initialized or not
+    private boolean mWebViewInitialized;
+
+    public static void initialize(final Context context) {    	
+        sInstance = new BrowserSettings(context);
+    }
+
+    public static BrowserSettings getInstance() {    	
+        return sInstance;
+    }
+
+    private BrowserSettings(Context context) {
+        mContext = context.getApplicationContext();
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+        mAutofillHandler = new AutofillHandler(mContext);
+        mManagedSettings = new LinkedList<WeakReference<WebSettings>>();
+        mCustomUserAgents = new WeakHashMap<WebSettings, String>();
+
+        // add for carrier feature
+        try {
+            sResPackageCtx = context.createPackageContext(
+                "com.android.browser.res",
+                Context.CONTEXT_IGNORE_SECURITY);
+        } catch (Exception e) {
+            Log.e("Res_Update", "Create Res Apk Failed");
+        }
+        BackgroundHandler.execute(mSetup);
+        mWebViewInitialized = false;
+    }
+
+    public void setController(Controller controller) {
+        mController = controller;
+        if (sInitialized) {
+            syncSharedSettings();
+        }
+    }
+
+    public void startManagingSettings(final WebSettings settings) {
+
+        if (mNeedsSharedSync) {
+            syncSharedSettings();
+        }
+
+        synchronized (mManagedSettings) {
+            syncStaticSettings(settings);
+            syncSetting(settings);
+            mManagedSettings.add(new WeakReference<WebSettings>(settings));
+        }
+    }
+
+    public void stopManagingSettings(WebSettings settings) {
+        Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+        while (iter.hasNext()) {
+            WeakReference<WebSettings> ref = iter.next();
+            if (ref.get() == settings) {
+                iter.remove();
+                return;
+            }
+        }
+    }
+
+    public void initializeCookieSettings() {
+        CookieManager.getInstance().setAcceptCookie(acceptCookies());
+        mWebViewInitialized = true;
+    }
+    private Runnable mSetup = new Runnable() {
+
+        @Override
+        public void run() {
+            DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
+            mFontSizeMult = metrics.scaledDensity / metrics.density;
+            // the cost of one cached page is ~3M (measured using nytimes.com). For
+            // low end devices, we only cache one page. For high end devices, we try
+            // to cache more pages, currently choose 5.
+
+            // SWE_TODO : assume a high-memory device
+            //if (ActivityManager.staticGetMemoryClass() > 16) {
+                mPageCacheCapacity = 5;
+            //}
+            mWebStorageSizeManager = new WebStorageSizeManager(mContext,
+                    new WebStorageSizeManager.StatFsDiskInfo(getAppCachePath()),
+                    new WebStorageSizeManager.WebKitAppCacheInfo(getAppCachePath()));
+            // Workaround b/5254577
+            mPrefs.registerOnSharedPreferenceChangeListener(BrowserSettings.this);
+            if (Build.VERSION.CODENAME.equals("REL")) {
+                // This is a release build, always startup with debug disabled
+                setDebugEnabled(false);
+            }
+            if (mPrefs.contains(PREF_TEXT_SIZE)) {
+                /*
+                 * Update from TextSize enum to zoom percent
+                 * SMALLEST is 50%
+                 * SMALLER is 75%
+                 * NORMAL is 100%
+                 * LARGER is 150%
+                 * LARGEST is 200%
+                 */
+                switch (getTextSize()) {
+                case SMALLEST:
+                    setTextZoom(50);
+                    break;
+                case SMALLER:
+                    setTextZoom(75);
+                    break;
+                case LARGER:
+                    setTextZoom(150);
+                    break;
+                case LARGEST:
+                    setTextZoom(200);
+                    break;
+                }
+                mPrefs.edit().remove(PREF_TEXT_SIZE).apply();
+            }
+
+            // add for carrier homepage feature
+            Object[] params  = { new String("persist.env.c.browser.resource"),
+                                 new String("default")};
+            Class[] type = new Class[] {String.class, String.class};
+            String browserRes = (String)ReflectHelper.invokeStaticMethod(
+                                "android.os.SystemProperties","get",type, params);
+            if ("cu".equals(browserRes) || "cmcc".equals(browserRes)) {
+                int resID = sResPackageCtx.getResources().getIdentifier(
+                        "homepage_base", "string", "com.android.browser.res");
+                sFactoryResetUrl = sResPackageCtx.getResources().getString(resID);
+            } else if ("ct".equals(browserRes)) {
+                int resID = sResPackageCtx.getResources().getIdentifier(
+                        "homepage_base", "string", "com.android.browser.res");
+                sFactoryResetUrl = sResPackageCtx.getResources().getString(resID);
+
+                int pathID = sResPackageCtx.getResources().getIdentifier(
+                        "homepage_path", "string", "com.android.browser.res");
+                String path = sResPackageCtx.getResources().getString(pathID);
+                Locale locale = Locale.getDefault();
+                path = path.replace("%y", locale.getLanguage().toLowerCase());
+                path = path.replace("%z", '_'+locale.getCountry().toLowerCase());
+                boolean useCountry = true;
+                boolean useLanguage = true;
+                InputStream is = null;
+                AssetManager am = mContext.getAssets();
+                try {
+                    is = am.open(path);
+                } catch (Exception ignored) {
+                    useCountry = false;
+                    path = sResPackageCtx.getResources().getString(pathID);
+                    path = path.replace("%y", locale.getLanguage().toLowerCase());
+                    path = path.replace("%z", "");
+                    try {
+                        is = am.open(path);
+                    } catch (Exception ignoredlanguage) {
+                        useLanguage = false;
+                    }
+                } finally {
+                    if (is != null) {
+                        try {
+                            is.close();
+                        } catch (Exception ignored) {}
+                    }
+                }
+
+                if (!useCountry && !useLanguage) {
+                    sFactoryResetUrl = sFactoryResetUrl.replace("%y%z", "en");
+                } else {
+                    sFactoryResetUrl = sFactoryResetUrl.replace("%y",
+                            locale.getLanguage().toLowerCase());
+                    sFactoryResetUrl = sFactoryResetUrl.replace("%z", useCountry ?
+                            '_' + locale.getCountry().toLowerCase() : "");
+                }
+            } else {
+                sFactoryResetUrl = mContext.getResources().getString(R.string.homepage_base);
+            }
+
+            if (!mPrefs.contains(PREF_DEFAULT_TEXT_ENCODING)) {
+                if (!"default".equals(browserRes)) {
+                    mPrefs.edit().putString(PREF_DEFAULT_TEXT_ENCODING,
+                            "GBK").apply();
+                }
+            }
+            if (sFactoryResetUrl.indexOf("{CID}") != -1) {
+                sFactoryResetUrl = sFactoryResetUrl.replace("{CID}",
+                    BrowserProvider.getClientId(mContext.getContentResolver()));
+            }
+
+            synchronized (BrowserSettings.class) {
+                sInitialized = true;
+                BrowserSettings.class.notifyAll();
+            }
+        }
+    };
+
+    private static void requireInitialization() {
+        synchronized (BrowserSettings.class) {
+            while (!sInitialized) {
+                try {
+                    BrowserSettings.class.wait();
+                } catch (InterruptedException e) {
+                }
+            }
+        }
+    }
+
+    /**
+     * Syncs all the settings that have a Preference UI
+     */
+    private void syncSetting(WebSettings settings) {
+        settings.setGeolocationEnabled(enableGeolocation());
+        settings.setJavaScriptEnabled(enableJavascript());
+        settings.setLightTouchEnabled(enableLightTouch());
+        settings.setNavDump(enableNavDump());
+        settings.setDefaultTextEncodingName(getDefaultTextEncoding());
+        settings.setDefaultZoom(getDefaultZoom());
+        settings.setMinimumFontSize(getMinimumFontSize());
+        settings.setMinimumLogicalFontSize(getMinimumFontSize());
+        settings.setPluginState(getPluginState());
+        settings.setTextZoom(getTextZoom());
+        settings.setLayoutAlgorithm(getLayoutAlgorithm());
+        settings.setJavaScriptCanOpenWindowsAutomatically(!blockPopupWindows());
+        settings.setLoadsImagesAutomatically(loadImages());
+        settings.setLoadWithOverviewMode(loadPageInOverviewMode());
+        settings.setSavePassword(rememberPasswords());
+        settings.setSaveFormData(saveFormdata());
+        settings.setUseWideViewPort(isWideViewport());
+
+        // add for carrier useragent feature
+        String ua = null;
+        try {
+            Class c = Class.forName("com.qrd.useragent.UserAgentHandler");
+            Object cObj = c.newInstance();
+            Method m = c.getDeclaredMethod("getUAString", Context.class);
+            ua = (String)m.invoke(cObj, mContext);
+        } catch (Exception e) {
+            Log.e(TAG, "plug in Load failed, err " + e);
+            ua = mCustomUserAgents.get(settings);
+        }
+        if (ua != null) {
+            settings.setUserAgentString(ua);
+        } else {
+            settings.setUserAgentString(USER_AGENTS[getUserAgent()]);
+        }
+
+        WebSettings settingsClassic = (WebSettings) settings;
+        settingsClassic.setHardwareAccelSkiaEnabled(isSkiaHardwareAccelerated());
+        settingsClassic.setShowVisualIndicator(enableVisualIndicator());
+        settingsClassic.setForceUserScalable(forceEnableUserScalable());
+        settingsClassic.setDoubleTapZoom(getDoubleTapZoom());
+        settingsClassic.setAutoFillEnabled(isAutofillEnabled());
+
+        boolean useInverted = useInvertedRendering();
+        settingsClassic.setProperty(WebViewProperties.gfxInvertedScreen,
+                useInverted ? "true" : "false");
+        if (useInverted) {
+          settingsClassic.setProperty(WebViewProperties.gfxInvertedScreenContrast,
+                    Float.toString(getInvertedContrast()));
+        }
+
+        if (isDebugEnabled()) {
+          settingsClassic.setProperty(WebViewProperties.gfxEnableCpuUploadPath,
+                    enableCpuUploadPath() ? "true" : "false");
+        }
+
+        settingsClassic.setLinkPrefetchEnabled(mLinkPrefetchAllowed);
+    }
+
+    /**
+     * Syncs all the settings that have no UI
+     * These cannot change, so we only need to set them once per WebSettings
+     */
+    private void syncStaticSettings(WebSettings settings) {
+        settings.setDefaultFontSize(16);
+        settings.setDefaultFixedFontSize(13);
+
+        // WebView inside Browser doesn't want initial focus to be set.
+        settings.setNeedInitialFocus(false);
+        // Browser supports multiple windows
+        settings.setSupportMultipleWindows(true);
+        // enable smooth transition for better performance during panning or
+        // zooming
+        settings.setEnableSmoothTransition(true);
+        // disable content url access
+        settings.setAllowContentAccess(false);
+
+        // HTML5 API flags
+        settings.setAppCacheEnabled(true);
+        settings.setDatabaseEnabled(true);
+        settings.setDomStorageEnabled(true);
+
+        // HTML5 configuration parametersettings.
+        settings.setAppCacheMaxSize(getWebStorageSizeManager().getAppCacheMaxSize());
+        settings.setAppCachePath(getAppCachePath());
+        settings.setDatabasePath(mContext.getDir("databases", 0).getPath());
+        settings.setGeolocationDatabasePath(mContext.getDir("geolocation", 0).getPath());
+        // origin policy for file access
+        settings.setAllowUniversalAccessFromFileURLs(false);
+        settings.setAllowFileAccessFromFileURLs(false);
+
+        //if (!(settings instanceof WebSettingsClassic)) return;
+        /*
+
+        WebSettingsClassic settingsClassic = (WebSettingsClassic) settings;
+        settingsClassic.setPageCacheCapacity(getPageCacheCapacity());
+        // WebView should be preserving the memory as much as possible.
+        // However, apps like browser wish to turn on the performance mode which
+        // would require more memory.
+        // TODO: We need to dynamically allocate/deallocate temporary memory for
+        // apps which are trying to use minimal memory. Currently, double
+        // buffering is always turned on, which is unnecessary.
+        settingsClassic.setProperty(WebViewProperties.gfxUseMinimalMemory, "false");
+        settingsClassic.setWorkersEnabled(true);  // This only affects V8.
+        */
+    }
+
+    private void syncSharedSettings() {
+        mNeedsSharedSync = false;
+        if (mWebViewInitialized) {
+            CookieManager.getInstance().setAcceptCookie(acceptCookies());
+        }
+        if (mController != null) {
+            mController.setShouldShowErrorConsole(enableJavascriptConsole());
+        }
+    }
+
+    private void syncManagedSettings() {
+        syncSharedSettings();
+        synchronized (mManagedSettings) {
+            Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+            while (iter.hasNext()) {
+                WeakReference<WebSettings> ref = iter.next();
+                WebSettings settings = (WebSettings)ref.get();
+                if (settings == null) {
+                    iter.remove();
+                    continue;
+                }
+                syncSetting(settings);
+            }
+        }
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(
+            SharedPreferences sharedPreferences, String key) {
+        syncManagedSettings();
+        if (PREF_SEARCH_ENGINE.equals(key)) {
+            updateSearchEngine(false);
+        } else if (PREF_FULLSCREEN.equals(key)) {
+            if (mController != null && mController.getUi() != null) {
+                mController.getUi().setFullscreen(useFullscreen());
+            }
+        } else if (PREF_ENABLE_QUICK_CONTROLS.equals(key)) {
+            if (mController != null && mController.getUi() != null) {
+                mController.getUi().setUseQuickControls(sharedPreferences.getBoolean(key, false));
+            }
+        } else if (PREF_LINK_PREFETCH.equals(key)) {
+            updateConnectionType();
+        }
+    }
+
+    public static String getFactoryResetHomeUrl(Context context) {
+        requireInitialization();
+        return sFactoryResetUrl;
+    }
+
+    public LayoutAlgorithm getLayoutAlgorithm() {
+        LayoutAlgorithm layoutAlgorithm = LayoutAlgorithm.NORMAL;
+        if (autofitPages()) {
+            layoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS;
+        }
+        if (isDebugEnabled()) {
+            if (isSmallScreen()) {
+                layoutAlgorithm = LayoutAlgorithm.SINGLE_COLUMN;
+            } else {
+                if (isNormalLayout()) {
+                    layoutAlgorithm = LayoutAlgorithm.NORMAL;
+                } else {
+                    layoutAlgorithm = LayoutAlgorithm.NARROW_COLUMNS;
+                }
+            }
+        }
+        return layoutAlgorithm;
+    }
+
+    public int getPageCacheCapacity() {
+        requireInitialization();
+        return mPageCacheCapacity;
+    }
+
+    public WebStorageSizeManager getWebStorageSizeManager() {
+        requireInitialization();
+        return mWebStorageSizeManager;
+    }
+
+    private String getAppCachePath() {
+        if (mAppCachePath == null) {
+            mAppCachePath = mContext.getDir("appcache", 0).getPath();
+        }
+        return mAppCachePath;
+    }
+
+    private void updateSearchEngine(boolean force) {
+        String searchEngineName = getSearchEngineName();
+        if (force || mSearchEngine == null ||
+                !mSearchEngine.getName().equals(searchEngineName)) {
+            mSearchEngine = SearchEngines.get(mContext, searchEngineName);
+         }
+    }
+
+    public SearchEngine getSearchEngine() {
+        if (mSearchEngine == null) {
+            updateSearchEngine(false);
+        }
+        return mSearchEngine;
+    }
+
+    public boolean isDebugEnabled() {
+        requireInitialization();
+        return mPrefs.getBoolean(PREF_DEBUG_MENU, false);
+    }
+
+    public void setDebugEnabled(boolean value) {
+        Editor edit = mPrefs.edit();
+        edit.putBoolean(PREF_DEBUG_MENU, value);
+        if (!value) {
+            // Reset to "safe" value
+            edit.putBoolean(PREF_ENABLE_HARDWARE_ACCEL_SKIA, false);
+        }
+        edit.apply();
+    }
+
+    public void clearCache() {
+        WebIconDatabase.getInstance().removeAllIcons();
+        if (mController != null) {
+            WebView current = mController.getCurrentWebView();
+            if (current != null) {
+                current.clearCache(true);
+            }
+        }
+    }
+
+    public void clearCookies() {
+        CookieManager.getInstance().removeAllCookie();
+    }
+
+    public void clearHistory() {
+        ContentResolver resolver = mContext.getContentResolver();
+        Browser.clearHistory(resolver);
+        Browser.clearSearches(resolver);
+    }
+
+    public void clearFormData() {
+        WebViewDatabase.getInstance(mContext).clearFormData();
+        if (mController!= null) {
+            WebView currentTopView = mController.getCurrentTopWebView();
+            if (currentTopView != null) {
+                currentTopView.clearFormData();
+            }
+        }
+    }
+
+    public void clearPasswords() {
+        // Clear password store maintained by SWE engine
+        WebSettings settings = null;
+        // find a valid settings object
+        Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+        while (iter.hasNext()) {
+            WeakReference<WebSettings> ref = iter.next();
+            settings = (WebSettings)ref.get();
+            if (settings != null) {
+                break;
+            }
+        }
+        if (settings != null) {
+            settings.clearPasswords();
+        }
+
+        // Clear passwords in WebView database
+        WebViewDatabase db = WebViewDatabase.getInstance(mContext);
+        db.clearUsernamePassword();
+        db.clearHttpAuthUsernamePassword();
+    }
+
+    public void clearDatabases() {
+        WebStorage.getInstance().deleteAllData();
+    }
+
+    public void clearLocationAccess() {
+        GeolocationPermissions.getInstance().clearAll();
+    }
+
+    public void resetDefaultPreferences() {
+        // Preserve autologin setting
+        long gal = mPrefs.getLong(GoogleAccountLogin.PREF_AUTOLOGIN_TIME, -1);
+        mPrefs.edit()
+                .clear()
+                .putLong(GoogleAccountLogin.PREF_AUTOLOGIN_TIME, gal)
+                .apply();
+        resetCachedValues();
+        syncManagedSettings();
+    }
+
+    private void resetCachedValues() {
+        updateSearchEngine(false);
+    }
+
+    public AutoFillProfile getAutoFillProfile() {
+         // query the profile from components autofill database 524
+        if (mAutofillHandler.mAutoFillProfile == null &&
+               !mAutofillHandler.mAutoFillActiveProfileId.equals("")) {
+            WebSettings settings = null;
+            // find a valid settings object
+            Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+            while (iter.hasNext()) {
+                WeakReference<WebSettings> ref = iter.next();
+                settings = (WebSettings)ref.get();
+                if (settings != null) {
+                    break;
+                }
+            }
+            if (settings != null) {
+                AutoFillProfile profile =
+                    settings.getAutoFillProfile(mAutofillHandler.mAutoFillActiveProfileId);
+                mAutofillHandler.setAutoFillProfile(profile);
+            }
+        }
+        return mAutofillHandler.getAutoFillProfile();
+    }
+
+    public String getAutoFillProfileId() {
+        return mAutofillHandler.getAutoFillProfileId();
+    }
+
+    public void updateAutoFillProfile(AutoFillProfile profile) {
+         syncAutoFillProfile(profile);
+    }
+
+    private void syncAutoFillProfile(AutoFillProfile profile) {
+       synchronized (mManagedSettings) {
+            Iterator<WeakReference<WebSettings>> iter = mManagedSettings.iterator();
+            while (iter.hasNext()) {
+                WeakReference<WebSettings> ref = iter.next();
+                WebSettings settings = (WebSettings)ref.get();
+                if (settings == null) {
+                    iter.remove();
+                    continue;
+                }
+                // update the profile only once.
+                settings.setAutoFillProfile(profile);
+                // Now we should have the guid
+                mAutofillHandler.setAutoFillProfile(profile);
+                break;
+            }
+        }
+    }
+    public void toggleDebugSettings() {
+        setDebugEnabled(!isDebugEnabled());
+    }
+
+    public boolean hasDesktopUseragent(WebView view) {
+        return view != null && mCustomUserAgents.get(view.getSettings()) != null;
+    }
+
+    public void toggleDesktopUseragent(WebView view) {
+        if (view == null) {
+            return;
+        }
+        WebSettings settings = view.getSettings();
+        if (mCustomUserAgents.get(settings) != null) {
+            mCustomUserAgents.remove(settings);
+            settings.setUserAgentString(USER_AGENTS[getUserAgent()]);
+        } else {
+            mCustomUserAgents.put(settings, DESKTOP_USERAGENT);
+            settings.setUserAgentString(DESKTOP_USERAGENT);
+        }
+    }
+
+    public static int getAdjustedMinimumFontSize(int rawValue) {
+        rawValue++; // Preference starts at 0, min font at 1
+        if (rawValue > 1) {
+            rawValue += (MIN_FONT_SIZE_OFFSET - 2);
+        }
+        return rawValue;
+    }
+
+    public int getAdjustedTextZoom(int rawValue) {
+        rawValue = (rawValue - TEXT_ZOOM_START_VAL) * TEXT_ZOOM_STEP;
+        return (int) ((rawValue + 100) * mFontSizeMult);
+    }
+
+    static int getRawTextZoom(int percent) {
+        return (percent - 100) / TEXT_ZOOM_STEP + TEXT_ZOOM_START_VAL;
+    }
+
+    public int getAdjustedDoubleTapZoom(int rawValue) {
+        rawValue = (rawValue - DOUBLE_TAP_ZOOM_START_VAL) * DOUBLE_TAP_ZOOM_STEP;
+        return (int) ((rawValue + 100) * mFontSizeMult);
+    }
+
+    static int getRawDoubleTapZoom(int percent) {
+        return (percent - 100) / DOUBLE_TAP_ZOOM_STEP + DOUBLE_TAP_ZOOM_START_VAL;
+    }
+
+    public SharedPreferences getPreferences() {
+        return mPrefs;
+    }
+
+    // update connectivity-dependent options
+    public void updateConnectionType() {
+        ConnectivityManager cm = (ConnectivityManager)
+            mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+        String linkPrefetchPreference = getLinkPrefetchEnabled();
+        boolean linkPrefetchAllowed = linkPrefetchPreference.
+            equals(getLinkPrefetchAlwaysPreferenceString(mContext));
+        NetworkInfo ni = cm.getActiveNetworkInfo();
+        if (ni != null) {
+            switch (ni.getType()) {
+                case ConnectivityManager.TYPE_WIFI:
+                case ConnectivityManager.TYPE_ETHERNET:
+                case ConnectivityManager.TYPE_BLUETOOTH:
+                    linkPrefetchAllowed |= linkPrefetchPreference.
+                        equals(getLinkPrefetchOnWifiOnlyPreferenceString(mContext));
+                    break;
+                case ConnectivityManager.TYPE_MOBILE:
+                case ConnectivityManager.TYPE_MOBILE_DUN:
+                case ConnectivityManager.TYPE_MOBILE_MMS:
+                case ConnectivityManager.TYPE_MOBILE_SUPL:
+                case ConnectivityManager.TYPE_WIMAX:
+                default:
+                    break;
+            }
+        }
+        if (mLinkPrefetchAllowed != linkPrefetchAllowed) {
+            mLinkPrefetchAllowed = linkPrefetchAllowed;
+            syncManagedSettings();
+        }
+    }
+
+    public String getDownloadPath() {
+        return mPrefs.getString(PREF_DOWNLOAD_PATH,
+                DownloadHandler.getDefaultDownloadPath(mContext));
+    }
+    // -----------------------------
+    // getter/setters for accessibility_preferences.xml
+    // -----------------------------
+
+    @Deprecated
+    private TextSize getTextSize() {
+        String textSize = mPrefs.getString(PREF_TEXT_SIZE, "NORMAL");
+        return TextSize.valueOf(textSize);
+    }
+
+    public int getMinimumFontSize() {
+        int minFont = mPrefs.getInt(PREF_MIN_FONT_SIZE, 0);
+        return getAdjustedMinimumFontSize(minFont);
+    }
+
+    public boolean forceEnableUserScalable() {
+        return mPrefs.getBoolean(PREF_FORCE_USERSCALABLE, false);
+    }
+
+    public int getTextZoom() {
+        requireInitialization();
+        int textZoom = mPrefs.getInt(PREF_TEXT_ZOOM, 10);
+        return getAdjustedTextZoom(textZoom);
+    }
+
+    public void setTextZoom(int percent) {
+        mPrefs.edit().putInt(PREF_TEXT_ZOOM, getRawTextZoom(percent)).apply();
+    }
+
+    public int getDoubleTapZoom() {
+        requireInitialization();
+        int doubleTapZoom = mPrefs.getInt(PREF_DOUBLE_TAP_ZOOM, 5);
+        return getAdjustedDoubleTapZoom(doubleTapZoom);
+    }
+
+    public void setDoubleTapZoom(int percent) {
+        mPrefs.edit().putInt(PREF_DOUBLE_TAP_ZOOM, getRawDoubleTapZoom(percent)).apply();
+    }
+
+    // -----------------------------
+    // getter/setters for advanced_preferences.xml
+    // -----------------------------
+
+    public String getSearchEngineName() {
+        String defaultSearchEngineValue = mContext.getString(R.string.default_search_engine_value);
+        if (defaultSearchEngineValue == null) {
+            defaultSearchEngineValue = SearchEngine.GOOGLE;
+        }
+        return mPrefs.getString(PREF_SEARCH_ENGINE, defaultSearchEngineValue);
+    }
+
+    public boolean allowAppTabs() {
+        return mPrefs.getBoolean(PREF_ALLOW_APP_TABS, false);
+    }
+
+    public boolean openInBackground() {
+        return mPrefs.getBoolean(PREF_OPEN_IN_BACKGROUND, false);
+    }
+
+    public boolean enableJavascript() {
+        return mPrefs.getBoolean(PREF_ENABLE_JAVASCRIPT, true);
+    }
+
+    public boolean enableMemoryMonitor() {
+        return mPrefs.getBoolean(PREF_ENABLE_MEMORY_MONITOR, true);
+    }
+
+    // TODO: Cache
+    public PluginState getPluginState() {
+        String state = mPrefs.getString(PREF_PLUGIN_STATE, "ON");
+        return PluginState.valueOf(state);
+    }
+
+    // TODO: Cache
+    public ZoomDensity getDefaultZoom() {
+        String zoom = mPrefs.getString(PREF_DEFAULT_ZOOM, "MEDIUM");
+        return ZoomDensity.valueOf(zoom);
+    }
+
+    public boolean loadPageInOverviewMode() {
+        return mPrefs.getBoolean(PREF_LOAD_PAGE, true);
+    }
+
+    public boolean autofitPages() {
+        return mPrefs.getBoolean(PREF_AUTOFIT_PAGES, true);
+    }
+
+    public boolean blockPopupWindows() {
+        return mPrefs.getBoolean(PREF_BLOCK_POPUP_WINDOWS, true);
+    }
+
+    public boolean loadImages() {
+        return mPrefs.getBoolean(PREF_LOAD_IMAGES, true);
+    }
+
+    public String getDefaultTextEncoding() {
+        return mPrefs.getString(PREF_DEFAULT_TEXT_ENCODING, null);
+    }
+
+    // -----------------------------
+    // getter/setters for general_preferences.xml
+    // -----------------------------
+
+    public String getHomePage() {
+        return mPrefs.getString(PREF_HOMEPAGE, getFactoryResetHomeUrl(mContext));
+    }
+
+    public void setHomePage(String value) {
+        mPrefs.edit().putString(PREF_HOMEPAGE, value).apply();
+    }
+
+    public boolean isAutofillEnabled() {
+        return mPrefs.getBoolean(PREF_AUTOFILL_ENABLED, true);
+    }
+
+    public void setAutofillEnabled(boolean value) {
+        mPrefs.edit().putBoolean(PREF_AUTOFILL_ENABLED, value).apply();
+    }
+
+    // -----------------------------
+    // getter/setters for debug_preferences.xml
+    // -----------------------------
+
+    public boolean isHardwareAccelerated() {
+        if (!isDebugEnabled()) {
+            return true;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_HARDWARE_ACCEL, true);
+    }
+
+    public boolean isSkiaHardwareAccelerated() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_HARDWARE_ACCEL_SKIA, false);
+    }
+
+    public int getUserAgent() {
+        if (!isDebugEnabled()) {
+            return 0;
+        }
+        return Integer.parseInt(mPrefs.getString(PREF_USER_AGENT, "0"));
+    }
+
+    // -----------------------------
+    // getter/setters for hidden_debug_preferences.xml
+    // -----------------------------
+
+    public boolean enableVisualIndicator() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_VISUAL_INDICATOR, false);
+    }
+
+    public boolean enableCpuUploadPath() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_CPU_UPLOAD_PATH, false);
+    }
+
+    public boolean enableJavascriptConsole() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_JAVASCRIPT_CONSOLE, true);
+    }
+
+    public boolean isSmallScreen() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_SMALL_SCREEN, false);
+    }
+
+    public boolean isWideViewport() {
+        if (!isDebugEnabled()) {
+            return true;
+        }
+        return mPrefs.getBoolean(PREF_WIDE_VIEWPORT, true);
+    }
+
+    public boolean isNormalLayout() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_NORMAL_LAYOUT, false);
+    }
+
+    public boolean isTracing() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_TRACING, false);
+    }
+
+    public boolean enableLightTouch() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_LIGHT_TOUCH, false);
+    }
+
+    public boolean enableNavDump() {
+        if (!isDebugEnabled()) {
+            return false;
+        }
+        return mPrefs.getBoolean(PREF_ENABLE_NAV_DUMP, false);
+    }
+
+    public String getJsEngineFlags() {
+        if (!isDebugEnabled()) {
+            return "";
+        }
+        return mPrefs.getString(PREF_JS_ENGINE_FLAGS, "");
+    }
+
+    // -----------------------------
+    // getter/setters for lab_preferences.xml
+    // -----------------------------
+
+    public boolean useQuickControls() {
+        return mPrefs.getBoolean(PREF_ENABLE_QUICK_CONTROLS, false);
+    }
+
+    public boolean useMostVisitedHomepage() {
+        return HomeProvider.MOST_VISITED.equals(getHomePage());
+    }
+
+    public boolean useFullscreen() {
+        return mPrefs.getBoolean(PREF_FULLSCREEN, false);
+    }
+
+    public boolean useInvertedRendering() {
+        return mPrefs.getBoolean(PREF_INVERTED, false);
+    }
+
+    public float getInvertedContrast() {
+        return 1 + (mPrefs.getInt(PREF_INVERTED_CONTRAST, 0) / 10f);
+    }
+
+    // -----------------------------
+    // getter/setters for privacy_security_preferences.xml
+    // -----------------------------
+
+    public boolean showSecurityWarnings() {
+        return mPrefs.getBoolean(PREF_SHOW_SECURITY_WARNINGS, true);
+    }
+
+    public boolean acceptCookies() {
+        return mPrefs.getBoolean(PREF_ACCEPT_COOKIES, true);
+    }
+
+    public boolean saveFormdata() {
+        return mPrefs.getBoolean(PREF_SAVE_FORMDATA, true);
+    }
+
+    public boolean enableGeolocation() {
+        return mPrefs.getBoolean(PREF_ENABLE_GEOLOCATION, true);
+    }
+
+    public boolean rememberPasswords() {
+        return mPrefs.getBoolean(PREF_REMEMBER_PASSWORDS, true);
+    }
+
+    // -----------------------------
+    // getter/setters for bandwidth_preferences.xml
+    // -----------------------------
+
+    public static String getPreloadOnWifiOnlyPreferenceString(Context context) {
+        return context.getResources().getString(R.string.pref_data_preload_value_wifi_only);
+    }
+
+    public static String getPreloadAlwaysPreferenceString(Context context) {
+        return context.getResources().getString(R.string.pref_data_preload_value_always);
+    }
+
+    private static final String DEAULT_PRELOAD_SECURE_SETTING_KEY =
+            "browser_default_preload_setting";
+
+    public String getDefaultPreloadSetting() {
+        String preload = Settings.Secure.getString(mContext.getContentResolver(),
+                DEAULT_PRELOAD_SECURE_SETTING_KEY);
+        if (preload == null) {
+            preload = mContext.getResources().getString(R.string.pref_data_preload_default_value);
+        }
+        return preload;
+    }
+
+    public String getPreloadEnabled() {
+        return mPrefs.getString(PREF_DATA_PRELOAD, getDefaultPreloadSetting());
+    }
+
+    public static String getLinkPrefetchOnWifiOnlyPreferenceString(Context context) {
+        return context.getResources().getString(R.string.pref_link_prefetch_value_wifi_only);
+    }
+
+    public static String getLinkPrefetchAlwaysPreferenceString(Context context) {
+        return context.getResources().getString(R.string.pref_link_prefetch_value_always);
+    }
+
+    private static final String DEFAULT_LINK_PREFETCH_SECURE_SETTING_KEY =
+            "browser_default_link_prefetch_setting";
+
+    public String getDefaultLinkPrefetchSetting() {
+        String preload = Settings.Secure.getString(mContext.getContentResolver(),
+            DEFAULT_LINK_PREFETCH_SECURE_SETTING_KEY);
+        if (preload == null) {
+            preload = mContext.getResources().getString(R.string.pref_link_prefetch_default_value);
+        }
+        return preload;
+    }
+
+    public String getLinkPrefetchEnabled() {
+        return mPrefs.getString(PREF_LINK_PREFETCH, getDefaultLinkPrefetchSetting());
+    }
+
+    // -----------------------------
+    // getter/setters for browser recovery
+    // -----------------------------
+    /**
+     * The last time browser was started.
+     * @return The last browser start time as System.currentTimeMillis. This
+     * can be 0 if this is the first time or the last tab was closed.
+     */
+    public long getLastRecovered() {
+        return mPrefs.getLong(KEY_LAST_RECOVERED, 0);
+    }
+
+    /**
+     * Sets the last browser start time.
+     * @param time The last time as System.currentTimeMillis that the browser
+     * was started. This should be set to 0 if the last tab is closed.
+     */
+    public void setLastRecovered(long time) {
+        mPrefs.edit()
+            .putLong(KEY_LAST_RECOVERED, time)
+            .apply();
+    }
+
+    /**
+     * Used to determine whether or not the previous browser run crashed. Once
+     * the previous state has been determined, the value will be set to false
+     * until a pause is received.
+     * @return true if the last browser run was paused or false if it crashed.
+     */
+    public boolean wasLastRunPaused() {
+        return mPrefs.getBoolean(KEY_LAST_RUN_PAUSED, false);
+    }
+
+    /**
+     * Sets whether or not the last run was a pause or crash.
+     * @param isPaused Set to true When a pause is received or false after
+     * resuming.
+     */
+    public void setLastRunPaused(boolean isPaused) {
+        mPrefs.edit()
+            .putBoolean(KEY_LAST_RUN_PAUSED, isPaused)
+            .apply();
+    }
+}
diff --git a/src/com/android/browser/BrowserSnapshotPage.java b/src/com/android/browser/BrowserSnapshotPage.java
new file mode 100644
index 0000000..5d2453b
--- /dev/null
+++ b/src/com/android/browser/BrowserSnapshotPage.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2011 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Fragment;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.ResourceCursorAdapter;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+public class BrowserSnapshotPage extends Fragment implements
+        LoaderCallbacks<Cursor>, OnItemClickListener {
+
+    public static final String EXTRA_ANIMATE_ID = "animate_id";
+
+    private static final int LOADER_SNAPSHOTS = 1;
+    private static final String[] PROJECTION = new String[] {
+        Snapshots._ID,
+        Snapshots.TITLE,
+        Snapshots.VIEWSTATE_SIZE,
+        Snapshots.THUMBNAIL,
+        Snapshots.FAVICON,
+        Snapshots.URL,
+        Snapshots.DATE_CREATED,
+    };
+    private static final int SNAPSHOT_ID = 0;
+    private static final int SNAPSHOT_TITLE = 1;
+    private static final int SNAPSHOT_VIEWSTATE_SIZE = 2;
+    private static final int SNAPSHOT_THUMBNAIL = 3;
+    private static final int SNAPSHOT_FAVICON = 4;
+    private static final int SNAPSHOT_URL = 5;
+    private static final int SNAPSHOT_DATE_CREATED = 6;
+
+    GridView mGrid;
+    View mEmpty;
+    SnapshotAdapter mAdapter;
+    CombinedBookmarksCallbacks mCallback;
+    long mAnimateId;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mCallback = (CombinedBookmarksCallbacks) getActivity();
+        mAnimateId = getArguments().getLong(EXTRA_ANIMATE_ID);
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.snapshots, container, false);
+        mEmpty = view.findViewById(android.R.id.empty);
+        mGrid = (GridView) view.findViewById(R.id.grid);
+        setupGrid(inflater);
+        getLoaderManager().initLoader(LOADER_SNAPSHOTS, null, this);
+        return view;
+    }
+
+    @Override
+    public void onDestroyView() {
+        super.onDestroyView();
+        getLoaderManager().destroyLoader(LOADER_SNAPSHOTS);
+        if (mAdapter != null) {
+            mAdapter.changeCursor(null);
+            mAdapter = null;
+        }
+    }
+
+    void setupGrid(LayoutInflater inflater) {
+        View item = inflater.inflate(R.layout.snapshot_item, mGrid, false);
+        int mspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        item.measure(mspec, mspec);
+        int width = item.getMeasuredWidth();
+        mGrid.setColumnWidth(width);
+        mGrid.setOnItemClickListener(this);
+        mGrid.setOnCreateContextMenuListener(this);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        if (id == LOADER_SNAPSHOTS) {
+            return new CursorLoader(getActivity(),
+                    Snapshots.CONTENT_URI, PROJECTION,
+                    null, null, Snapshots.DATE_CREATED + " DESC");
+        }
+        return null;
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
+        if (loader.getId() == LOADER_SNAPSHOTS) {
+            if (mAdapter == null) {
+                mAdapter = new SnapshotAdapter(getActivity(), data);
+                mGrid.setAdapter(mAdapter);
+            } else {
+                mAdapter.changeCursor(data);
+            }
+            if (mAnimateId > 0) {
+                mAdapter.animateIn(mAnimateId);
+                mAnimateId = 0;
+                getArguments().remove(EXTRA_ANIMATE_ID);
+            }
+            boolean empty = mAdapter.isEmpty();
+            mGrid.setVisibility(empty ? View.GONE : View.VISIBLE);
+            mEmpty.setVisibility(empty ? View.VISIBLE : View.GONE);
+        }
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v,
+            ContextMenuInfo menuInfo) {
+        MenuInflater inflater = getActivity().getMenuInflater();
+        inflater.inflate(R.menu.snapshots_context, menu);
+        // Create the header, re-use BookmarkItem (has the layout we want)
+        BookmarkItem header = new BookmarkItem(getActivity());
+        header.setEnableScrolling(true);
+        AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
+        populateBookmarkItem(mAdapter.getItem(info.position), header);
+        menu.setHeaderView(header);
+    }
+
+    private void populateBookmarkItem(Cursor cursor, BookmarkItem item) {
+        item.setName(cursor.getString(SNAPSHOT_TITLE));
+        item.setUrl(cursor.getString(SNAPSHOT_URL));
+        item.setFavicon(getBitmap(cursor, SNAPSHOT_FAVICON));
+    }
+
+    static Bitmap getBitmap(Cursor cursor, int columnIndex) {
+        byte[] data = cursor.getBlob(columnIndex);
+        if (data == null) {
+            return null;
+        }
+        return BitmapFactory.decodeByteArray(data, 0, data.length);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        if (!(item.getMenuInfo() instanceof AdapterContextMenuInfo)) {
+            return false;
+        }
+        if (item.getItemId() == R.id.delete_context_menu_id) {
+            AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
+            deleteSnapshot(info.id);
+            return true;
+        }
+        return super.onContextItemSelected(item);
+    }
+
+    void deleteSnapshot(long id) {
+        final Uri uri = ContentUris.withAppendedId(Snapshots.CONTENT_URI, id);
+        final ContentResolver cr = getActivity().getContentResolver();
+        new Thread() {
+            @Override
+            public void run() {
+                cr.delete(uri, null, null);
+            }
+        }.start();
+
+    }
+
+    @Override
+    public void onItemClick(AdapterView<?> parent, View view, int position,
+            long id) {
+        mCallback.openSnapshot(id);
+    }
+
+    private static class SnapshotAdapter extends ResourceCursorAdapter {
+        private long mAnimateId;
+        private AnimatorSet mAnimation;
+        private View mAnimationTarget;
+
+        public SnapshotAdapter(Context context, Cursor c) {
+            super(context, R.layout.snapshot_item, c, 0);
+            mAnimation = new AnimatorSet();
+            mAnimation.playTogether(
+                    ObjectAnimator.ofFloat(null, View.SCALE_X, 0f, 1f),
+                    ObjectAnimator.ofFloat(null, View.SCALE_Y, 0f, 1f));
+            mAnimation.setStartDelay(100);
+            mAnimation.setDuration(400);
+            mAnimation.addListener(new AnimatorListener() {
+
+                @Override
+                public void onAnimationStart(Animator animation) {
+                }
+
+                @Override
+                public void onAnimationRepeat(Animator animation) {
+                }
+
+                @Override
+                public void onAnimationEnd(Animator animation) {
+                    mAnimateId = 0;
+                    mAnimationTarget = null;
+                }
+
+                @Override
+                public void onAnimationCancel(Animator animation) {
+                }
+            });
+        }
+
+        public void animateIn(long id) {
+            mAnimateId = id;
+        }
+
+        @Override
+        public void bindView(View view, Context context, Cursor cursor) {
+            long id = cursor.getLong(SNAPSHOT_ID);
+            if (id == mAnimateId) {
+                if (mAnimationTarget != view) {
+                    float scale = 0f;
+                    if (mAnimationTarget != null) {
+                        scale = mAnimationTarget.getScaleX();
+                        mAnimationTarget.setScaleX(1f);
+                        mAnimationTarget.setScaleY(1f);
+                    }
+                    view.setScaleX(scale);
+                    view.setScaleY(scale);
+                }
+                mAnimation.setTarget(view);
+                mAnimationTarget = view;
+                if (!mAnimation.isRunning()) {
+                    mAnimation.start();
+                }
+
+            }
+            ImageView thumbnail = (ImageView) view.findViewById(R.id.thumb);
+            byte[] thumbBlob = cursor.getBlob(SNAPSHOT_THUMBNAIL);
+            if (thumbBlob == null) {
+                thumbnail.setImageResource(R.drawable.browser_thumbnail);
+            } else {
+                Bitmap thumbBitmap = BitmapFactory.decodeByteArray(
+                        thumbBlob, 0, thumbBlob.length);
+                thumbnail.setImageBitmap(thumbBitmap);
+            }
+            TextView title = (TextView) view.findViewById(R.id.title);
+            title.setText(cursor.getString(SNAPSHOT_TITLE));
+            TextView size = (TextView) view.findViewById(R.id.size);
+            if (size != null) {
+                int stateLen = cursor.getInt(SNAPSHOT_VIEWSTATE_SIZE);
+                size.setText(String.format("%.2fMB", stateLen / 1024f / 1024f));
+            }
+            long timestamp = cursor.getLong(SNAPSHOT_DATE_CREATED);
+            TextView date = (TextView) view.findViewById(R.id.date);
+            DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
+            date.setText(dateFormat.format(new Date(timestamp)));
+        }
+
+        @Override
+        public Cursor getItem(int position) {
+            return (Cursor) super.getItem(position);
+        }
+    }
+
+}
diff --git a/src/com/android/browser/BrowserUtils.java b/src/com/android/browser/BrowserUtils.java
new file mode 100644
index 0000000..be16ab1
--- /dev/null
+++ b/src/com/android/browser/BrowserUtils.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *     * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser;
+
+import com.android.browser.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.InputFilter;
+import android.text.Spanned;
+import android.util.Log;
+import android.widget.EditText;
+
+public class BrowserUtils {
+
+    private static final String LOGTAG = "BrowserUtils";
+    public static final int FILENAME_MAX_LENGTH = 32;
+    public static final int ADDRESS_MAX_LENGTH = 2048;
+    private static AlertDialog.Builder mAlertDialog = null;
+
+    public static void maxLengthFilter(final Context context, final EditText editText,
+            final int max_length) {
+        InputFilter[] contentFilters = new InputFilter[1];
+        contentFilters[0] = new InputFilter.LengthFilter(max_length) {
+            public CharSequence filter(CharSequence source, int start, int end,
+                    Spanned dest, int dstart, int dend) {
+                int keep = max_length - (dest.length() - (dend - dstart));
+                if (keep <= 0) {
+                    showWarningDialog(context, max_length);
+                    return "";
+                } else if (keep >= end - start) {
+                    return null;
+                } else {
+                    if (keep < source.length()) {
+                        showWarningDialog(context, max_length);
+                    }
+                    return source.subSequence(start, start + keep);
+                }
+            }
+        };
+        editText.setFilters(contentFilters);
+    }
+
+    private static void showWarningDialog(final Context context, int max_length) {
+        if (mAlertDialog != null)
+            return;
+
+        mAlertDialog = new AlertDialog.Builder(context);
+        mAlertDialog.setTitle(R.string.browser_max_input_title)
+                .setIcon(android.R.drawable.ic_dialog_info)
+                .setMessage(context.getString(R.string.browser_max_input, max_length))
+                .setPositiveButton(R.string.ok,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog, int which) {
+                                return;
+                            }
+                        })
+                .show()
+                .setOnDismissListener(new DialogInterface.OnDismissListener() {
+                    public void onDismiss(DialogInterface dialog) {
+                        Log.w("BrowserUtils", "onDismiss");
+                        mAlertDialog = null;
+                        return;
+                    }
+                });
+    }
+}
diff --git a/src/com/android/browser/BrowserWebView.java b/src/com/android/browser/BrowserWebView.java
new file mode 100644
index 0000000..5d71ce3
--- /dev/null
+++ b/src/com/android/browser/BrowserWebView.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2010 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.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebViewClient;
+
+import java.util.Map;
+
+/**
+ * Manage WebView scroll events
+ */
+public class BrowserWebView extends WebView implements WebView.TitleBarDelegate {
+
+    public interface OnScrollChangedListener {
+        void onScrollChanged(int l, int t, int oldl, int oldt);
+    }
+
+    private boolean mBackgroundRemoved = false;
+    private TitleBar mTitleBar;
+    private OnScrollChangedListener mOnScrollChangedListener;
+    private WebChromeClient mWebChromeClient;
+    private WebViewClient mWebViewClient;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     * @param javascriptInterfaces
+     */
+    public BrowserWebView(Context context, AttributeSet attrs, int defStyle,
+            Map<String, Object> javascriptInterfaces, boolean privateBrowsing) {
+        super(context, attrs, defStyle, privateBrowsing);
+        this.setJavascriptInterfaces(javascriptInterfaces);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public BrowserWebView(
+            Context context, AttributeSet attrs, int defStyle, boolean privateBrowsing) {
+        super(context, attrs, defStyle, privateBrowsing);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public BrowserWebView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /**
+     * @param context
+     */
+    public BrowserWebView(Context context) {
+        super(context);
+    }
+
+    @Override
+    public void setWebChromeClient(WebChromeClient client) {
+        mWebChromeClient = client;
+        super.setWebChromeClient(client);
+    }
+
+    public WebChromeClient getWebChromeClient() {
+      return mWebChromeClient;
+    }
+
+    @Override
+    public void setWebViewClient(WebViewClient client) {
+        mWebViewClient = client;
+        super.setWebViewClient(client);
+    }
+
+    public WebViewClient getWebViewClient() {
+      return mWebViewClient;
+    }
+
+    public void setTitleBar(TitleBar title) {
+        mTitleBar = title;
+    }
+
+    // From TitleBarDelegate
+    @Override
+    public int getTitleHeight() {
+        return (mTitleBar != null) ? mTitleBar.getEmbeddedHeight() : 0;
+    }
+
+    // From TitleBarDelegate
+    @Override
+    public void onSetEmbeddedTitleBar(final View title) {
+        // TODO: Remove this method; it is never invoked.
+    }
+
+    public boolean hasTitleBar() {
+        return (mTitleBar != null);
+    }
+
+    @Override
+    public void onDraw(Canvas c) {
+        super.onDraw(c);
+        if (!mBackgroundRemoved && getRootView().getBackground() != null) {
+            mBackgroundRemoved = true;
+            post(new Runnable() {
+                public void run() {
+                    getRootView().setBackgroundDrawable(null);
+                }
+            });
+        }
+    }
+
+    public void drawContent(Canvas c) {
+        //super.drawContent(c);
+    }
+
+    @Override
+    public void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+        if (mTitleBar != null) {
+            mTitleBar.onScrollChanged();
+        }
+        if (mOnScrollChangedListener != null) {
+            mOnScrollChangedListener.onScrollChanged(l, t, oldl, oldt);
+        }
+    }
+
+    public void setOnScrollChangedListener(OnScrollChangedListener listener) {
+        mOnScrollChangedListener = listener;
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView) {
+        return false;
+    }
+
+    @Override
+    public void destroy() {
+        BrowserSettings.getInstance().stopManagingSettings(getSettings());
+        super.destroy();
+    }
+
+}
diff --git a/src/com/android/browser/BrowserWebViewFactory.java b/src/com/android/browser/BrowserWebViewFactory.java
new file mode 100644
index 0000000..4364b26
--- /dev/null
+++ b/src/com/android/browser/BrowserWebViewFactory.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.content.pm.PackageManager;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.WebView;
+
+/**
+ * Web view factory class for creating {@link BrowserWebView}'s.
+ */
+public class BrowserWebViewFactory implements WebViewFactory {
+
+    private final Context mContext;
+
+    public BrowserWebViewFactory(Context context) {
+        mContext = context;
+    }
+
+    protected WebView instantiateWebView(AttributeSet attrs, int defStyle,
+            boolean privateBrowsing) {
+        return new BrowserWebView(mContext, attrs, defStyle, privateBrowsing);
+    }
+
+    @Override
+    public WebView createSubWebView(boolean privateBrowsing) {
+        return createWebView(privateBrowsing);
+    }
+
+    @Override
+    public WebView createWebView(boolean privateBrowsing) {
+        WebView w = instantiateWebView(null, android.R.attr.webViewStyle, privateBrowsing);
+        initWebViewSettings(w);
+        return w;
+    }
+
+    protected void initWebViewSettings(WebView w) {
+        w.setScrollbarFadingEnabled(true);
+        w.setScrollBarStyle(View.SCROLLBARS_OUTSIDE_OVERLAY);
+        w.setMapTrackballToArrowKeys(false); // use trackball directly
+        // Enable the built-in zoom
+        w.getSettings().setBuiltInZoomControls(true);
+        final PackageManager pm = mContext.getPackageManager();
+        boolean supportsMultiTouch =
+                pm.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH)
+                || pm.hasSystemFeature(PackageManager.FEATURE_FAKETOUCH_MULTITOUCH_DISTINCT);
+        w.getSettings().setDisplayZoomControls(!supportsMultiTouch);
+
+        // add for carrier homepage feature
+        Object[] params  = {new String("persist.env.c.browser.resource"),
+                            new String("default")};
+        Class[] type = new Class[] {String.class, String.class};
+        String browserRes = (String)ReflectHelper.invokeStaticMethod(
+                            "android.os.SystemProperties","get", type, params);
+        if ("ct".equals(browserRes)) {
+            w.getSettings().setJavaScriptEnabled(true);
+            if (mContext instanceof BrowserActivity) {
+                w.addJavascriptInterface(mContext, "default_homepage");
+            }
+        }
+
+        // Add this WebView to the settings observer list and update the
+        // settings
+        final BrowserSettings s = BrowserSettings.getInstance();
+        s.startManagingSettings(w.getSettings());
+    }
+
+}
diff --git a/src/com/android/browser/BrowserYesNoPreference.java b/src/com/android/browser/BrowserYesNoPreference.java
new file mode 100644
index 0000000..f2344b4
--- /dev/null
+++ b/src/com/android/browser/BrowserYesNoPreference.java
@@ -0,0 +1,60 @@
+/*
+ * 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.Context;
+import android.preference.DialogPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+class BrowserYesNoPreference extends DialogPreference {
+
+    // This is the constructor called by the inflater
+    public BrowserYesNoPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onDialogClosed(boolean positiveResult) {
+        super.onDialogClosed(positiveResult);
+
+        if (positiveResult) {
+            setEnabled(false);
+
+            BrowserSettings settings = BrowserSettings.getInstance();
+            if (PreferenceKeys.PREF_PRIVACY_CLEAR_CACHE.equals(getKey())) {
+                settings.clearCache();
+                settings.clearDatabases();
+            } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_COOKIES.equals(getKey())) {
+                settings.clearCookies();
+            } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY.equals(getKey())) {
+                settings.clearHistory();
+            } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_FORM_DATA.equals(getKey())) {
+                settings.clearFormData();
+            } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_PASSWORDS.equals(getKey())) {
+                settings.clearPasswords();
+            } else if (PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES.equals(
+                    getKey())) {
+                settings.resetDefaultPreferences();
+                setEnabled(true);
+            } else if (PreferenceKeys.PREF_PRIVACY_CLEAR_GEOLOCATION_ACCESS.equals(
+                    getKey())) {
+                settings.clearLocationAccess();
+            }
+        }
+    }
+}
diff --git a/src/com/android/browser/CombinedBookmarksCallbacks.java b/src/com/android/browser/CombinedBookmarksCallbacks.java
new file mode 100644
index 0000000..cdffb6b
--- /dev/null
+++ b/src/com/android/browser/CombinedBookmarksCallbacks.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface CombinedBookmarksCallbacks {
+    void openUrl(String url);
+    void openInNewTab(String... urls);
+    void openSnapshot(long id);
+    void close();
+}
\ No newline at end of file
diff --git a/src/com/android/browser/ComboViewActivity.java b/src/com/android/browser/ComboViewActivity.java
new file mode 100644
index 0000000..4026bdd
--- /dev/null
+++ b/src/com/android/browser/ComboViewActivity.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2011 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.ActionBar;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v13.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+
+import java.util.ArrayList;
+
+public class ComboViewActivity extends Activity implements CombinedBookmarksCallbacks {
+
+    private static final String STATE_SELECTED_TAB = "tab";
+    public static final String EXTRA_COMBO_ARGS = "combo_args";
+    public static final String EXTRA_INITIAL_VIEW = "initial_view";
+
+    public static final String EXTRA_OPEN_SNAPSHOT = "snapshot_id";
+    public static final String EXTRA_OPEN_ALL = "open_all";
+    public static final String EXTRA_CURRENT_URL = "url";
+    private ViewPager mViewPager;
+    private TabsAdapter mTabsAdapter;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setResult(RESULT_CANCELED);
+        Bundle extras = getIntent().getExtras();
+        Bundle args = extras.getBundle(EXTRA_COMBO_ARGS);
+        String svStr = extras.getString(EXTRA_INITIAL_VIEW, null);
+        ComboViews startingView = svStr != null
+                ? ComboViews.valueOf(svStr)
+                : ComboViews.Bookmarks;
+        mViewPager = new ViewPager(this);
+        mViewPager.setId(R.id.tab_view);
+        setContentView(mViewPager);
+
+        final ActionBar bar = getActionBar();
+        bar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+        if (BrowserActivity.isTablet(this)) {
+            bar.setDisplayOptions(ActionBar.DISPLAY_SHOW_HOME
+                    | ActionBar.DISPLAY_USE_LOGO);
+            bar.setHomeButtonEnabled(true);
+        } else {
+            bar.setDisplayOptions(0);
+        }
+
+        mTabsAdapter = new TabsAdapter(this, mViewPager);
+        mTabsAdapter.addTab(bar.newTab().setText(R.string.tab_bookmarks),
+                BrowserBookmarksPage.class, args);
+        mTabsAdapter.addTab(bar.newTab().setText(R.string.tab_history),
+                BrowserHistoryPage.class, args);
+        mTabsAdapter.addTab(bar.newTab().setText(R.string.tab_snapshots),
+                BrowserSnapshotPage.class, args);
+
+        if (savedInstanceState != null) {
+            bar.setSelectedNavigationItem(
+                    savedInstanceState.getInt(STATE_SELECTED_TAB, 0));
+        } else {
+            switch (startingView) {
+            case Bookmarks:
+                mViewPager.setCurrentItem(0);
+                break;
+            case History:
+                mViewPager.setCurrentItem(1);
+                break;
+            case Snapshots:
+                mViewPager.setCurrentItem(2);
+                break;
+            }
+        }
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(STATE_SELECTED_TAB,
+                getActionBar().getSelectedNavigationIndex());
+    }
+
+    @Override
+    public void openUrl(String url) {
+        Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+        setResult(RESULT_OK, i);
+        finish();
+    }
+
+    @Override
+    public void openInNewTab(String... urls) {
+        Intent i = new Intent();
+        i.putExtra(EXTRA_OPEN_ALL, urls);
+        setResult(RESULT_OK, i);
+        finish();
+    }
+
+    @Override
+    public void close() {
+        finish();
+    }
+
+    @Override
+    public void openSnapshot(long id) {
+        Intent i = new Intent();
+        i.putExtra(EXTRA_OPEN_SNAPSHOT, id);
+        setResult(RESULT_OK, i);
+        finish();
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        getMenuInflater().inflate(R.menu.combined, menu);
+        return super.onCreateOptionsMenu(menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (item.getItemId() == android.R.id.home) {
+            finish();
+            return true;
+        } else if (item.getItemId() == R.id.preferences_menu_id) {
+            String url = getIntent().getStringExtra(EXTRA_CURRENT_URL);
+            Intent intent = new Intent(this, BrowserPreferencesPage.class);
+            intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE, url);
+            startActivityForResult(intent, Controller.PREFERENCES_PAGE);
+            return true;
+        }
+        return super.onOptionsItemSelected(item);
+    }
+
+    /**
+     * This is a helper class that implements the management of tabs and all
+     * details of connecting a ViewPager with associated TabHost.  It relies on a
+     * trick.  Normally a tab host has a simple API for supplying a View or
+     * Intent that each tab will show.  This is not sufficient for switching
+     * between pages.  So instead we make the content part of the tab host
+     * 0dp high (it is not shown) and the TabsAdapter supplies its own dummy
+     * view to show as the tab content.  It listens to changes in tabs, and takes
+     * care of switch to the correct page in the ViewPager whenever the selected
+     * tab changes.
+     */
+    public static class TabsAdapter extends FragmentPagerAdapter
+            implements ActionBar.TabListener, ViewPager.OnPageChangeListener {
+        private final Context mContext;
+        private final ActionBar mActionBar;
+        private final ViewPager mViewPager;
+        private final ArrayList<TabInfo> mTabs = new ArrayList<TabInfo>();
+
+        static final class TabInfo {
+            private final Class<?> clss;
+            private final Bundle args;
+
+            TabInfo(Class<?> _class, Bundle _args) {
+                clss = _class;
+                args = _args;
+            }
+        }
+
+        public TabsAdapter(Activity activity, ViewPager pager) {
+            super(activity.getFragmentManager());
+            mContext = activity;
+            mActionBar = activity.getActionBar();
+            mViewPager = pager;
+            mViewPager.setAdapter(this);
+            mViewPager.setOnPageChangeListener(this);
+        }
+
+        public void addTab(ActionBar.Tab tab, Class<?> clss, Bundle args) {
+            TabInfo info = new TabInfo(clss, args);
+            tab.setTag(info);
+            tab.setTabListener(this);
+            mTabs.add(info);
+            mActionBar.addTab(tab);
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return mTabs.size();
+        }
+
+        @Override
+        public Fragment getItem(int position) {
+            TabInfo info = mTabs.get(position);
+            return Fragment.instantiate(mContext, info.clss.getName(), info.args);
+        }
+
+        @Override
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+        }
+
+        @Override
+        public void onPageSelected(int position) {
+            mActionBar.setSelectedNavigationItem(position);
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {
+        }
+
+        @Override
+        public void onTabSelected(android.app.ActionBar.Tab tab,
+                FragmentTransaction ft) {
+            Object tag = tab.getTag();
+            for (int i=0; i<mTabs.size(); i++) {
+                if (mTabs.get(i) == tag) {
+                    mViewPager.setCurrentItem(i);
+                }
+            }
+        }
+
+        @Override
+        public void onTabUnselected(android.app.ActionBar.Tab tab,
+                FragmentTransaction ft) {
+        }
+
+        @Override
+        public void onTabReselected(android.app.ActionBar.Tab tab,
+                FragmentTransaction ft) {
+        }
+    }
+
+    private static String makeFragmentName(int viewId, int index) {
+        return "android:switcher:" + viewId + ":" + index;
+    }
+
+}
diff --git a/src/com/android/browser/Controller.java b/src/com/android/browser/Controller.java
new file mode 100644
index 0000000..27b3620
--- /dev/null
+++ b/src/com/android/browser/Controller.java
@@ -0,0 +1,3368 @@
+/*
+ * Copyright (C) 2010 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.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DownloadManager;
+import android.app.ProgressDialog;
+import android.content.ClipboardManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.net.wifi.WifiManager;
+import android.net.wifi.ScanResult;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.preference.PreferenceActivity;
+import android.provider.Browser;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Intents.Insert;
+import android.provider.Settings;
+import android.speech.RecognizerIntent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.MotionEvent;
+import android.view.View;
+import android.webkit.MimeTypeMap;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.webkit.WebIconDatabase;
+import android.widget.Toast;
+
+import org.codeaurora.swe.CookieManager;
+import org.codeaurora.swe.CookieSyncManager;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+import com.android.browser.IntentHandler.UrlData;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.mynavigation.AddMyNavigationPage;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.platformsupport.BrowserContract.Images;
+import com.android.browser.provider.BrowserProvider2.Thumbnails;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Controller for browser
+ */
+public class Controller
+        implements WebViewController, UiController, ActivityController {
+
+    private static final String LOGTAG = "Controller";
+    private static final String SEND_APP_ID_EXTRA =
+        "android.speech.extras.SEND_APPLICATION_ID_EXTRA";
+    private static final String INCOGNITO_URI = "browser:incognito";
+
+
+    private static final String PROP_NETSWITCH = "persist.env.browser.netswitch";
+    private static final String INTENT_WIFI_SELECTION_DATA_CONNECTION =
+            "android.net.wifi.cmcc.WIFI_SELECTION_DATA_CONNECTION";
+    private static final String OFFLINE_PAGE =
+            "content://com.android.browser.mynavigation/websites";
+    private static final String INTENT_PICK_NETWORK =
+            "android.net.wifi.cmcc.PICK_WIFI_NETWORK_AND_GPRS";
+
+    public final static String EXTRA_SHARE_SCREENSHOT = "share_screenshot";
+    public final static String EXTRA_SHARE_FAVICON = "share_favicon";
+    // public message ids
+    public final static int LOAD_URL = 1001;
+    public final static int STOP_LOAD = 1002;
+
+    // Message Ids
+    private static final int FOCUS_NODE_HREF = 102;
+    private static final int RELEASE_WAKELOCK = 107;
+
+    static final int UPDATE_BOOKMARK_THUMBNAIL = 108;
+
+    private static final int OPEN_BOOKMARKS = 201;
+    private static final int OPEN_MENU = 202;
+
+    private static final int EMPTY_MENU = -1;
+
+    // activity requestCode
+    final static int COMBO_VIEW = 1;
+    final static int PREFERENCES_PAGE = 3;
+    final static int FILE_SELECTED = 4;
+    final static int AUTOFILL_SETUP = 5;
+    final static int VOICE_RESULT = 6;
+    final static int MY_NAVIGATION = 7;
+
+    private final static int WAKELOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes
+
+    // 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 };
+
+    // "source" parameter for Google search through search key
+    final static String GOOGLE_SEARCH_SOURCE_SEARCHKEY = "browser-key";
+    // "source" parameter for Google search through simplily type
+    final static String GOOGLE_SEARCH_SOURCE_TYPE = "browser-type";
+
+    // "no-crash-recovery" parameter in intent to suppress crash recovery
+    final static String NO_CRASH_RECOVERY = "no-crash-recovery";
+
+    // A bitmap that is re-used in createScreenshot as scratch space
+    private static Bitmap sThumbnailBitmap;
+
+    private Activity mActivity;
+    private UI mUi;
+    private TabControl mTabControl;
+    private BrowserSettings mSettings;
+    private WebViewFactory mFactory;
+
+    private WakeLock mWakeLock;
+
+    private UrlHandler mUrlHandler;
+    private UploadHandler mUploadHandler;
+    private IntentHandler mIntentHandler;
+    private PageDialogsHandler mPageDialogsHandler;
+    private NetworkStateHandler mNetworkHandler;
+
+    private Message mAutoFillSetupMessage;
+
+    private boolean mShouldShowErrorConsole;
+    private boolean mNetworkShouldNotify = true;
+
+    private SystemAllowGeolocationOrigins mSystemAllowGeolocationOrigins;
+
+    // FIXME, temp address onPrepareMenu performance problem.
+    // When we move everything out of view, we should rewrite this.
+    private int mCurrentMenuState = 0;
+    private int mMenuState = R.id.MAIN_MENU;
+    private int mOldMenuState = EMPTY_MENU;
+    private Menu mCachedMenu;
+
+    private boolean mMenuIsDown;
+
+    // For select and find, we keep track of the ActionMode so that
+    // finish() can be called as desired.
+    private ActionMode mActionMode;
+
+    /**
+     * Only meaningful when mOptionsMenuOpen is true.  This variable keeps track
+     * of whether the configuration has changed.  The first onMenuOpened call
+     * after a configuration change is simply a reopening of the same menu
+     * (i.e. mIconView did not change).
+     */
+    private boolean mConfigChanged;
+
+    /**
+     * Keeps track of whether the options menu is open. This is important in
+     * determining whether to show or hide the title bar overlay
+     */
+    private boolean mOptionsMenuOpen;
+
+    /**
+     * Whether or not the options menu is in its bigger, popup menu form. When
+     * true, we want the title bar overlay to be gone. When false, we do not.
+     * Only meaningful if mOptionsMenuOpen is true.
+     */
+    private boolean mExtendedMenuOpen;
+
+    private boolean mActivityPaused = true;
+    private boolean mLoadStopped;
+
+    private Handler mHandler;
+    // Checks to see when the bookmarks database has changed, and updates the
+    // Tabs' notion of whether they represent bookmarked sites.
+    private ContentObserver mBookmarksObserver;
+    private CrashRecoveryHandler mCrashRecoveryHandler;
+
+    private boolean mBlockEvents;
+
+    private String mVoiceResult;
+    private boolean mUpdateMyNavThumbnail;
+    private String mUpdateMyNavThumbnailUrl;
+
+    public Controller(Activity browser) {
+        mActivity = browser;
+        mSettings = BrowserSettings.getInstance();
+        mTabControl = new TabControl(this);
+        mSettings.setController(this);
+        mCrashRecoveryHandler = CrashRecoveryHandler.initialize(this);
+        mCrashRecoveryHandler.preloadCrashState();
+        mFactory = new BrowserWebViewFactory(browser);
+
+        mUrlHandler = new UrlHandler(this);
+        mIntentHandler = new IntentHandler(mActivity, this);
+        mPageDialogsHandler = new PageDialogsHandler(mActivity, this);
+
+        // Creating dummy Webview for browser to force loading of library;
+        // in order for CookieManager calls to be invoked properly and
+        // awBrowserContext to be initialized
+        (mFactory.createWebView(false)).destroy();
+
+        startHandler();
+        mBookmarksObserver = new ContentObserver(mHandler) {
+            @Override
+            public void onChange(boolean selfChange) {
+                int size = mTabControl.getTabCount();
+                for (int i = 0; i < size; i++) {
+                    mTabControl.getTab(i).updateBookmarkedStatus();
+                }
+            }
+
+        };
+        browser.getContentResolver().registerContentObserver(
+                BrowserContract.Bookmarks.CONTENT_URI, true, mBookmarksObserver);
+
+        mNetworkHandler = new NetworkStateHandler(mActivity, this);
+        // Start watching the default geolocation permissions
+        mSystemAllowGeolocationOrigins =
+                new SystemAllowGeolocationOrigins(mActivity.getApplicationContext());
+        mSystemAllowGeolocationOrigins.start();
+
+        openIconDatabase();
+    }
+
+    @Override
+    public void start(final Intent intent) {
+        WebView.setShouldMonitorWebCoreThread();
+        // mCrashRecoverHandler has any previously saved state.
+        mCrashRecoveryHandler.startRecovery(intent);
+    }
+
+    void doStart(final Bundle icicle, final Intent intent) {
+        // Unless the last browser usage was within 24 hours, destroy any
+        // remaining incognito tabs.
+
+        Calendar lastActiveDate = icicle != null ?
+                (Calendar) icicle.getSerializable("lastActiveDate") : null;
+        Calendar today = Calendar.getInstance();
+        Calendar yesterday = Calendar.getInstance();
+        yesterday.add(Calendar.DATE, -1);
+
+        // we dont want to ever recover incognito tabs
+        final boolean restoreIncognitoTabs = false;
+
+        // Find out if we will restore any state and remember the tab.
+        final long currentTabId =
+                mTabControl.canRestoreState(icicle, restoreIncognitoTabs);
+
+        mSettings.initializeCookieSettings();
+        if (currentTabId == -1) {
+            // Not able to restore so we go ahead and clear session cookies.  We
+            // must do this before trying to login the user as we don't want to
+            // clear any session cookies set during login.
+            CookieManager.getInstance().removeSessionCookie();
+        }
+
+        GoogleAccountLogin.startLoginIfNeeded(mActivity,
+                new Runnable() {
+                    @Override public void run() {
+                        onPreloginFinished(icicle, intent, currentTabId,
+                                restoreIncognitoTabs);
+                    }
+                });
+    }
+
+    private void onPreloginFinished(Bundle icicle, Intent intent, long currentTabId,
+            boolean restoreIncognitoTabs) {
+        if (currentTabId == -1) {
+            BackgroundHandler.execute(new PruneThumbnails(mActivity, null));
+            if (intent == null) {
+                // This won't happen under common scenarios. The icicle is
+                // not null, but there aren't any tabs to restore.
+                openTabToHomePage();
+            } else {
+                final Bundle extra = intent.getExtras();
+                // Create an initial tab.
+                // 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.
+                UrlData urlData = null;
+                if (intent.getData() != null
+                        && Intent.ACTION_VIEW.equals(intent.getAction())
+                        && intent.getData().toString().startsWith("content://")) {
+                    urlData = new UrlData(intent.getData().toString());
+                } else {
+                    urlData = IntentHandler.getUrlDataFromIntent(intent);
+                }
+                Tab t = null;
+                if (urlData.isEmpty()) {
+                    Object[] params  = { new String("persist.env.c.browser.resource"),
+                                 new String("default")};
+                    Class[] type = new Class[] {String.class, String.class};
+                    String browserRes = (String)ReflectHelper.invokeStaticMethod(
+                        "android.os.SystemProperties", "get",
+                        type, params);
+                    if (browserRes.equals(
+                            "cmcc")) {
+                        t = openTab(OFFLINE_PAGE, false, true, true);
+                    } else {
+                        t = openTabToHomePage();
+                    }
+                } else {
+                    t = openTab(urlData);
+                }
+                if (t != null) {
+                    t.setAppId(intent.getStringExtra(Browser.EXTRA_APPLICATION_ID));
+                }
+                WebView webView = t.getWebView();
+                if (extra != null) {
+                    int scale = extra.getInt(Browser.INITIAL_ZOOM_LEVEL, 0);
+                    if (scale > 0 && scale <= 1000) {
+                        webView.setInitialScale(scale);
+                    }
+                }
+            }
+            mUi.updateTabs(mTabControl.getTabs());
+        } else {
+            mTabControl.restoreState(icicle, currentTabId, restoreIncognitoTabs,
+                    mUi.needsRestoreAllTabs());
+            List<Tab> tabs = mTabControl.getTabs();
+            ArrayList<Long> restoredTabs = new ArrayList<Long>(tabs.size());
+            for (Tab t : tabs) {
+                restoredTabs.add(t.getId());
+            }
+            BackgroundHandler.execute(new PruneThumbnails(mActivity, restoredTabs));
+            if (tabs.size() == 0) {
+                openTabToHomePage();
+            }
+            mUi.updateTabs(tabs);
+            // TabControl.restoreState() will create a new tab even if
+            // restoring the state fails.
+            setActiveTab(mTabControl.getCurrentTab());
+            // Intent is non-null when framework thinks the browser should be
+            // launching with a new intent (icicle is null).
+            if (intent != null) {
+                mIntentHandler.onNewIntent(intent);
+            }
+        }
+        // Read JavaScript flags if it exists.
+        String jsFlags = getSettings().getJsEngineFlags();
+        if (jsFlags.trim().length() != 0) {
+            getCurrentWebView().setJsFlags(jsFlags);
+        }
+        if (intent != null
+                && BrowserActivity.ACTION_SHOW_BOOKMARKS.equals(intent.getAction())) {
+            bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+        }
+    }
+
+    private static class PruneThumbnails implements Runnable {
+        private Context mContext;
+        private List<Long> mIds;
+
+        PruneThumbnails(Context context, List<Long> preserveIds) {
+            mContext = context.getApplicationContext();
+            mIds = preserveIds;
+        }
+
+        @Override
+        public void run() {
+            ContentResolver cr = mContext.getContentResolver();
+            if (mIds == null || mIds.size() == 0) {
+                cr.delete(Thumbnails.CONTENT_URI, null, null);
+            } else {
+                int length = mIds.size();
+                StringBuilder where = new StringBuilder();
+                where.append(Thumbnails._ID);
+                where.append(" not in (");
+                for (int i = 0; i < length; i++) {
+                    where.append(mIds.get(i));
+                    if (i < (length - 1)) {
+                        where.append(",");
+                    }
+                }
+                where.append(")");
+                cr.delete(Thumbnails.CONTENT_URI, where.toString(), null);
+            }
+        }
+
+    }
+
+    @Override
+    public WebViewFactory getWebViewFactory() {
+        return mFactory;
+    }
+
+    @Override
+    public void onSetWebView(Tab tab, WebView view) {
+        if (tab.hasCrashed)
+            tab.showCrashView();
+        else
+            mUi.onSetWebView(tab, view);
+    }
+
+    @Override
+    public void createSubWindow(Tab tab) {
+        endActionMode();
+        WebView mainView = tab.getWebView();
+        WebView subView = mFactory.createWebView((mainView == null)
+                ? false
+                : mainView.isPrivateBrowsingEnabled());
+        mUi.createSubWindow(tab, subView);
+    }
+
+    @Override
+    public Context getContext() {
+        return mActivity;
+    }
+
+    @Override
+    public Activity getActivity() {
+        return mActivity;
+    }
+
+    void setUi(UI ui) {
+        mUi = ui;
+    }
+
+    @Override
+    public BrowserSettings getSettings() {
+        return mSettings;
+    }
+
+    IntentHandler getIntentHandler() {
+        return mIntentHandler;
+    }
+
+    @Override
+    public UI getUi() {
+        return mUi;
+    }
+
+    int getMaxTabs() {
+        return mActivity.getResources().getInteger(R.integer.max_tabs);
+    }
+
+    @Override
+    public TabControl getTabControl() {
+        return mTabControl;
+    }
+
+    @Override
+    public List<Tab> getTabs() {
+        return mTabControl.getTabs();
+    }
+
+    // Open the icon database.
+    private void openIconDatabase() {
+        // We have to call getInstance on the UI thread
+        final WebIconDatabase instance = WebIconDatabase.getInstance();
+        BackgroundHandler.execute(new Runnable() {
+
+            @Override
+            public void run() {
+                instance.open(mActivity.getDir("icons", 0).getPath());
+            }
+        });
+    }
+
+    private void startHandler() {
+        mHandler = new Handler() {
+
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                    case OPEN_BOOKMARKS:
+                        bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+                        break;
+                    case FOCUS_NODE_HREF:
+                    {
+                        String url = (String) msg.getData().get("url");
+                        String title = (String) msg.getData().get("title");
+                        String src = (String) msg.getData().get("src");
+                        if (url == "") url = src; // use image if no anchor
+                        if (TextUtils.isEmpty(url)) {
+                            break;
+                        }
+                        HashMap focusNodeMap = (HashMap) msg.obj;
+                        WebView view = (WebView) focusNodeMap.get("webview");
+                        // Only apply the action if the top window did not change.
+                        if (getCurrentTopWebView() != view) {
+                            break;
+                        }
+                        switch (msg.arg1) {
+                            case R.id.open_context_menu_id:
+                                loadUrlFromContext(url);
+                                break;
+                            case R.id.view_image_context_menu_id:
+                                loadUrlFromContext(src);
+                                break;
+                            case R.id.open_newtab_context_menu_id:
+                                final Tab parent = mTabControl.getCurrentTab();
+                                openTab(url, parent,
+                                        !mSettings.openInBackground(), true);
+                                break;
+                            case R.id.copy_link_context_menu_id:
+                                copy(url);
+                                break;
+                            case R.id.save_link_context_menu_id:
+                            case R.id.download_context_menu_id:
+                                DownloadHandler.onDownloadStartNoStream(
+                                  mActivity, url, view.getSettings().getUserAgentString(),
+                                  null, null, null, view.isPrivateBrowsingEnabled(), 0);
+                                break;
+                        }
+                        break;
+                    }
+
+                    case LOAD_URL:
+                        loadUrlFromContext((String) msg.obj);
+                        break;
+
+                    case STOP_LOAD:
+                        stopLoading();
+                        break;
+
+                    case RELEASE_WAKELOCK:
+                        if (mWakeLock != null && mWakeLock.isHeld()) {
+                            mWakeLock.release();
+                            // if we reach here, Browser should be still in the
+                            // background loading after WAKELOCK_TIMEOUT (5-min).
+                            // To avoid burning the battery, stop loading.
+                            mTabControl.stopAllLoading();
+                        }
+                        break;
+
+                    case UPDATE_BOOKMARK_THUMBNAIL:
+                        Tab tab = (Tab) msg.obj;
+                        if (tab != null) {
+                            updateScreenshot(tab);
+                        }
+                        break;
+                    case OPEN_MENU:
+                        if (!mOptionsMenuOpen && mActivity != null ) {
+                            mActivity.openOptionsMenu();
+                        }
+                        break;
+                }
+            }
+        };
+
+    }
+
+    @Override
+    public Tab getCurrentTab() {
+        return mTabControl.getCurrentTab();
+    }
+
+    @Override
+    public void shareCurrentPage() {
+        shareCurrentPage(mTabControl.getCurrentTab());
+    }
+
+    private void shareCurrentPage(Tab tab) {
+        if (tab != null) {
+            sharePage(mActivity, tab.getTitle(),
+                    tab.getUrl(), tab.getFavicon(),
+                    createScreenshot(tab.getWebView(),
+                            getDesiredThumbnailWidth(mActivity),
+                            getDesiredThumbnailHeight(mActivity)));
+        }
+    }
+
+    /**
+     * Share a page, providing the title, url, favicon, and a screenshot.  Uses
+     * an {@link Intent} to launch the Activity chooser.
+     * @param c Context used to launch a new Activity.
+     * @param title Title of the page.  Stored in the Intent with
+     *          {@link Intent#EXTRA_SUBJECT}
+     * @param url URL of the page.  Stored in the Intent with
+     *          {@link Intent#EXTRA_TEXT}
+     * @param favicon Bitmap of the favicon for the page.  Stored in the Intent
+     *          with {@link Browser#EXTRA_SHARE_FAVICON}
+     * @param screenshot Bitmap of a screenshot of the page.  Stored in the
+     *          Intent with {@link Browser#EXTRA_SHARE_SCREENSHOT}
+     */
+    static final void sharePage(Context c, String title, String url,
+            Bitmap favicon, Bitmap screenshot) {
+        Intent send = new Intent(Intent.ACTION_SEND);
+        send.setType("text/plain");
+        send.putExtra(Intent.EXTRA_TEXT, url);
+        send.putExtra(Intent.EXTRA_SUBJECT, title);
+        send.putExtra(EXTRA_SHARE_FAVICON, favicon);
+        send.putExtra(EXTRA_SHARE_SCREENSHOT, screenshot);
+        try {
+            c.startActivity(Intent.createChooser(send, c.getString(
+                    R.string.choosertitle_sharevia)));
+        } catch(android.content.ActivityNotFoundException ex) {
+            // if no app handles it, do nothing
+        }
+    }
+
+    private void copy(CharSequence text) {
+        ClipboardManager cm = (ClipboardManager) mActivity
+                .getSystemService(Context.CLIPBOARD_SERVICE);
+        cm.setText(text);
+    }
+
+    // lifecycle
+
+    @Override
+    public void onConfgurationChanged(Configuration config) {
+        mConfigChanged = true;
+        // update the menu in case of a locale change
+        mActivity.invalidateOptionsMenu();
+        if (mOptionsMenuOpen) {
+            mActivity.closeOptionsMenu();
+            mHandler.sendMessageDelayed(mHandler.obtainMessage(OPEN_MENU), 100);
+        }
+        if (mPageDialogsHandler != null) {
+            mPageDialogsHandler.onConfigurationChanged(config);
+        }
+        mUi.onConfigurationChanged(config);
+    }
+
+    @Override
+    public void handleNewIntent(Intent intent) {
+        if (!mUi.isWebShowing()) {
+            mUi.showWeb(false);
+        }
+        mIntentHandler.onNewIntent(intent);
+    }
+
+    @Override
+    public void onPause() {
+        if (mUi.isCustomViewShowing()) {
+            hideCustomView();
+        }
+        if (mActivityPaused) {
+            Log.e(LOGTAG, "BrowserActivity is already paused.");
+            return;
+        }
+        mActivityPaused = true;
+        Tab tab = mTabControl.getCurrentTab();
+        if (tab != null) {
+            tab.pause();
+            if (!pauseWebViewTimers(tab)) {
+                if (mWakeLock == null) {
+                    PowerManager pm = (PowerManager) mActivity
+                            .getSystemService(Context.POWER_SERVICE);
+                    mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Browser");
+                }
+                mWakeLock.acquire();
+                mHandler.sendMessageDelayed(mHandler
+                        .obtainMessage(RELEASE_WAKELOCK), WAKELOCK_TIMEOUT);
+            }
+        }
+        mUi.onPause();
+        mNetworkHandler.onPause();
+
+        WebView.disablePlatformNotifications();
+        NfcHandler.unregister(mActivity);
+        if (sThumbnailBitmap != null) {
+            sThumbnailBitmap.recycle();
+            sThumbnailBitmap = null;
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+        // Save all the tabs
+        Bundle saveState = createSaveState();
+
+        // crash recovery manages all save & restore state
+        mCrashRecoveryHandler.writeState(saveState);
+        mSettings.setLastRunPaused(true);
+    }
+
+    /**
+     * Save the current state to outState. Does not write the state to
+     * disk.
+     * @return Bundle containing the current state of all tabs.
+     */
+    /* package */ Bundle createSaveState() {
+        Bundle saveState = new Bundle();
+        mTabControl.saveState(saveState);
+        if (!saveState.isEmpty()) {
+            // Save time so that we know how old incognito tabs (if any) are.
+            saveState.putSerializable("lastActiveDate", Calendar.getInstance());
+        }
+        return saveState;
+    }
+
+    @Override
+    public void onResume() {
+        if (!mActivityPaused) {
+            Log.e(LOGTAG, "BrowserActivity is already resumed.");
+            return;
+        }
+        mSettings.setLastRunPaused(false);
+        mActivityPaused = false;
+        Tab current = mTabControl.getCurrentTab();
+        if (current != null) {
+            current.resume();
+            resumeWebViewTimers(current);
+        }
+        releaseWakeLock();
+
+        mUi.onResume();
+        mNetworkHandler.onResume();
+        WebView.enablePlatformNotifications();
+        NfcHandler.register(mActivity, this);
+        if (mVoiceResult != null) {
+            mUi.onVoiceResult(mVoiceResult);
+            mVoiceResult = null;
+        }
+        if (current != null && current.hasCrashed) {
+            current.showCrashView();
+        }
+    }
+
+    private void releaseWakeLock() {
+        if (mWakeLock != null && mWakeLock.isHeld()) {
+            mHandler.removeMessages(RELEASE_WAKELOCK);
+            mWakeLock.release();
+        }
+    }
+
+    /**
+     * resume all WebView timers using the WebView instance of the given tab
+     * @param tab guaranteed non-null
+     */
+    private void resumeWebViewTimers(Tab tab) {
+        boolean inLoad = tab.inPageLoad();
+        if ((!mActivityPaused && !inLoad) || (mActivityPaused && inLoad)) {
+            CookieSyncManager.getInstance().startSync();
+            WebView w = tab.getWebView();
+            WebViewTimersControl.getInstance().onBrowserActivityResume(w);
+        }
+    }
+
+    /**
+     * Pause all WebView timers using the WebView of the given tab
+     * @param tab
+     * @return true if the timers are paused or tab is null
+     */
+    private boolean pauseWebViewTimers(Tab tab) {
+        if (tab == null) {
+            return true;
+        } else if (!tab.inPageLoad()) {
+            CookieSyncManager.getInstance().stopSync();
+            WebViewTimersControl.getInstance().onBrowserActivityPause(getCurrentWebView());
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onDestroy() {
+        if (mUploadHandler != null && !mUploadHandler.handled()) {
+            mUploadHandler.onResult(Activity.RESULT_CANCELED, null);
+            mUploadHandler = null;
+        }
+        if (mTabControl == null) return;
+        mUi.onDestroy();
+        // Remove the current tab and sub window
+        Tab t = mTabControl.getCurrentTab();
+        if (t != null) {
+            dismissSubWindow(t);
+            removeTab(t);
+        }
+        mActivity.getContentResolver().unregisterContentObserver(mBookmarksObserver);
+        // Destroy all the tabs
+        mTabControl.destroy();
+        WebIconDatabase.getInstance().close();
+        // Stop watching the default geolocation permissions
+        mSystemAllowGeolocationOrigins.stop();
+        mSystemAllowGeolocationOrigins = null;
+    }
+
+    protected boolean isActivityPaused() {
+        return mActivityPaused;
+    }
+
+    @Override
+    public void onLowMemory() {
+        mTabControl.freeMemory();
+    }
+
+    @Override
+    public boolean shouldShowErrorConsole() {
+        return mShouldShowErrorConsole;
+    }
+
+    protected void setShouldShowErrorConsole(boolean show) {
+        if (show == mShouldShowErrorConsole) {
+            // Nothing to do.
+            return;
+        }
+        mShouldShowErrorConsole = show;
+        Tab t = mTabControl.getCurrentTab();
+        if (t == null) {
+            // There is no current tab so we cannot toggle the error console
+            return;
+        }
+        mUi.setShouldShowErrorConsole(t, show);
+    }
+
+    @Override
+    public void stopLoading() {
+        mLoadStopped = true;
+        Tab tab = mTabControl.getCurrentTab();
+        WebView w = getCurrentTopWebView();
+        if (w != null) {
+            w.stopLoading();
+            mUi.onPageStopped(tab);
+        }
+    }
+
+    boolean didUserStopLoading() {
+        return mLoadStopped;
+    }
+
+    private void handleNetworkNotify(WebView view) {
+        ConnectivityManager conMgr = (ConnectivityManager) this.getContext().getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        WifiManager wifiMgr = (WifiManager) this.getContext()
+                .getSystemService(Context.WIFI_SERVICE);
+        int networkSwitchTypeOK = this.getContext().getResources()
+                .getInteger(R.integer.netswitch_type_remind);
+
+        if (wifiMgr.isWifiEnabled()) {
+            NetworkInfo mNetworkInfo = conMgr.getActiveNetworkInfo();
+            if (mNetworkInfo == null
+                    || (mNetworkInfo != null && (mNetworkInfo.getType() !=
+                    ConnectivityManager.TYPE_WIFI))) {
+                List<ScanResult> list = wifiMgr.getScanResults();
+                if (list != null && list.size() == 0) {
+                    int isReminder = Settings.System.getInt(
+                            mActivity.getContentResolver(),
+                            this.getContext().getResources()
+                                    .getString(R.string.network_switch_remind_type),
+                            networkSwitchTypeOK);
+                    if (isReminder == networkSwitchTypeOK) {
+                        Intent intent = new Intent(
+                                INTENT_WIFI_SELECTION_DATA_CONNECTION);
+                        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                        this.getContext().startActivity(intent);
+                    }
+                } else {
+                    if ((Boolean)ReflectHelper.invokeStaticMethod(
+                             "ActivityManagerNative", "isSystemReady", null, null)) {
+                        try {
+                            Intent intent = new Intent(INTENT_PICK_NETWORK);
+                            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                            this.getContext().startActivity(intent);
+                        } catch (Exception e) {
+                            String err_msg = this.getContext().getString(
+                                    R.string.acivity_not_found, INTENT_PICK_NETWORK);
+                            Toast.makeText(this.getContext(), err_msg, Toast.LENGTH_LONG).show();
+                        }
+                    } else {
+                        Log.e(LOGTAG, "System is not ready!");
+                    }
+                }
+                mNetworkShouldNotify = false;
+            }
+        } else {
+            if (!mNetworkHandler.isNetworkUp()) {
+                view.setNetworkAvailable(false);
+                Log.v(LOGTAG, "handleNetworkNotify() Wlan is not enabled.");
+            }
+        }
+    }
+    // WebViewController
+
+    @Override
+    public void onPageStarted(Tab tab, WebView view, Bitmap favicon) {
+
+        // We've started to load a new page. If there was a pending message
+        // to save a screenshot then we will now take the new page and save
+        // an incorrect screenshot. Therefore, remove any pending thumbnail
+        // messages from the queue.
+        mHandler.removeMessages(Controller.UPDATE_BOOKMARK_THUMBNAIL,
+                tab);
+
+        // reset sync timer to avoid sync starts during loading a page
+        CookieSyncManager.getInstance().resetSync();
+        Object[] params  = {new String(PROP_NETSWITCH),
+                            new Boolean(false)};
+        Class[] type = new Class[] {String.class, boolean.class};
+        Boolean result = (Boolean) ReflectHelper.invokeStaticMethod(
+                        "android.os.SystemProperties", "getBoolean",
+                        type, params);
+        if (result) {
+            if (!mNetworkHandler.isNetworkUp()) {
+                Log.d(LOGTAG, "onPageStarted() network unavailable");
+                if (mNetworkShouldNotify) {
+                    handleNetworkNotify(view);
+                } else {
+                    view.setNetworkAvailable(false);
+                }
+                mNetworkShouldNotify = false;
+            } else {
+                Log.d(LOGTAG, "onPageStarted() network available");
+                if (mNetworkShouldNotify) {
+                    handleNetworkNotify(view);
+                }
+                mNetworkShouldNotify = false;
+            }
+        } else {
+            if (!mNetworkHandler.isNetworkUp()) {
+                view.setNetworkAvailable(false);
+            }
+        }
+
+        // when BrowserActivity just starts, onPageStarted may be called before
+        // onResume as it is triggered from onCreate. Call resumeWebViewTimers
+        // to start the timer. As we won't switch tabs while an activity is in
+        // pause state, we can ensure calling resume and pause in pair.
+        if (mActivityPaused) {
+            resumeWebViewTimers(tab);
+        }
+        mLoadStopped = false;
+        endActionMode();
+
+        mUi.onTabDataChanged(tab);
+
+        String url = tab.getUrl();
+        // update the bookmark database for favicon
+        maybeUpdateFavicon(tab, null, url, favicon);
+
+        Performance.tracePageStart(url);
+
+        // Performance probe
+        if (false) {
+            Performance.onPageStarted();
+        }
+
+    }
+
+    @Override
+    public void onPageFinished(Tab tab) {
+        mCrashRecoveryHandler.backupState();
+        mUi.onTabDataChanged(tab);
+
+        // Performance probe
+        if (false) {
+            Performance.onPageFinished(tab.getUrl());
+         }
+
+        Performance.tracePageFinished();
+    }
+
+    @Override
+    public void onProgressChanged(Tab tab) {
+        int newProgress = tab.getLoadProgress();
+
+        if (newProgress == 100) {
+            CookieSyncManager.getInstance().sync();
+            // onProgressChanged() may continue to be called after the main
+            // frame has finished loading, as any remaining sub frames continue
+            // to load. We'll only get called once though with newProgress as
+            // 100 when everything is loaded. (onPageFinished is called once
+            // when the main frame completes loading regardless of the state of
+            // any sub frames so calls to onProgressChanges may continue after
+            // onPageFinished has executed)
+            if (tab.inPageLoad()) {
+                updateInLoadMenuItems(mCachedMenu, tab);
+            } else if (mActivityPaused && pauseWebViewTimers(tab)) {
+                // pause the WebView timer and release the wake lock if it is
+                // finished while BrowserActivity is in pause state.
+                releaseWakeLock();
+            }
+            if (!tab.isPrivateBrowsingEnabled()
+                    && !TextUtils.isEmpty(tab.getUrl())
+                    && !tab.isSnapshot()) {
+                // Only update the bookmark screenshot if the user did not
+                // cancel the load early and there is not already
+                // a pending update for the tab.
+                if (tab.shouldUpdateThumbnail() &&
+                        (tab.inForeground() && !didUserStopLoading()
+                        || !tab.inForeground())) {
+                    if (!mHandler.hasMessages(UPDATE_BOOKMARK_THUMBNAIL, tab)) {
+                        mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                                UPDATE_BOOKMARK_THUMBNAIL, 0, 0, tab),
+                                1500);
+                    }
+                }
+            }
+        } else {
+            if (!tab.inPageLoad()) {
+                // onPageFinished may have already been called but a subframe is
+                // still loading
+                // updating the progress and
+                // update the menu items.
+                updateInLoadMenuItems(mCachedMenu, tab);
+            }
+        }
+        mUi.onProgressChanged(tab);
+    }
+
+    @Override
+    public void onUpdatedSecurityState(Tab tab) {
+        mUi.onTabDataChanged(tab);
+    }
+
+    @Override
+    public void onReceivedTitle(Tab tab, final String title) {
+        mUi.onTabDataChanged(tab);
+        final String pageUrl = tab.getOriginalUrl();
+        if (TextUtils.isEmpty(pageUrl) || pageUrl.length()
+                >= SQLiteDatabase.SQLITE_MAX_LIKE_PATTERN_LENGTH) {
+            return;
+        }
+        // Update the title in the history database if not in private browsing mode
+        if (!tab.isPrivateBrowsingEnabled()) {
+            DataController.getInstance(mActivity).updateHistoryTitle(pageUrl, title);
+        }
+    }
+
+    @Override
+    public void onFavicon(Tab tab, WebView view, Bitmap icon) {
+        mUi.onTabDataChanged(tab);
+        maybeUpdateFavicon(tab, view.getOriginalUrl(), view.getUrl(), icon);
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+        return mUrlHandler.shouldOverrideUrlLoading(tab, view, url);
+    }
+
+    @Override
+    public boolean shouldOverrideKeyEvent(KeyEvent event) {
+        if (mMenuIsDown) {
+            // only check shortcut key when MENU is held
+            return mActivity.getWindow().isShortcutKey(event.getKeyCode(),
+                    event);
+        }
+        int keyCode = event.getKeyCode();
+        // We need to send almost every key to WebKit. However:
+        // 1. We don't want to block the device on the renderer for
+        // some keys like menu, home, call.
+        // 2. There are no WebKit equivalents for some of these keys
+        // (see app/keyboard_codes_win.h)
+        // Note that these are not the same set as KeyEvent.isSystemKey:
+        // for instance, AKEYCODE_MEDIA_* will be dispatched to webkit.
+        if (keyCode == KeyEvent.KEYCODE_MENU ||
+            keyCode == KeyEvent.KEYCODE_HOME ||
+            keyCode == KeyEvent.KEYCODE_BACK ||
+            keyCode == KeyEvent.KEYCODE_CALL ||
+            keyCode == KeyEvent.KEYCODE_ENDCALL ||
+            keyCode == KeyEvent.KEYCODE_POWER ||
+            keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
+            keyCode == KeyEvent.KEYCODE_CAMERA ||
+            keyCode == KeyEvent.KEYCODE_FOCUS ||
+            keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ||
+            keyCode == KeyEvent.KEYCODE_VOLUME_MUTE ||
+            keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+            return true;
+        }
+
+        // We also have to intercept some shortcuts before we send them to the ContentView.
+        if (event.isCtrlPressed() && (
+                keyCode == KeyEvent.KEYCODE_TAB ||
+                keyCode == KeyEvent.KEYCODE_W ||
+                keyCode == KeyEvent.KEYCODE_F4)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onUnhandledKeyEvent(KeyEvent event) {
+        if (!isActivityPaused()) {
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                return mActivity.onKeyDown(event.getKeyCode(), event);
+            } else {
+                return mActivity.onKeyUp(event.getKeyCode(), event);
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void doUpdateVisitedHistory(Tab tab, boolean isReload) {
+        // Don't save anything in private browsing mode
+        if (tab.isPrivateBrowsingEnabled()) return;
+        String url = tab.getOriginalUrl();
+
+        if (TextUtils.isEmpty(url)
+                || url.regionMatches(true, 0, "about:", 0, 6)) {
+            return;
+        }
+
+        DataController.getInstance(mActivity).updateVisitedHistory(url);
+        mCrashRecoveryHandler.backupState();
+    }
+
+    @Override
+    public void getVisitedHistory(final ValueCallback<String[]> callback) {
+        AsyncTask<Void, Void, String[]> task =
+                new AsyncTask<Void, Void, String[]>() {
+            @Override
+            public String[] doInBackground(Void... unused) {
+                Object[] params  = {mActivity.getContentResolver()};
+                Class[] type = new Class[] {ContentResolver.class};
+                return (String[])ReflectHelper.invokeStaticMethod(
+                    "android.provider.Browser","getVisitedHistory",
+                    type, params);
+            }
+            @Override
+            public void onPostExecute(String[] result) {
+                callback.onReceiveValue(result);
+            }
+        };
+        task.execute();
+    }
+
+    @Override
+    public void onReceivedHttpAuthRequest(Tab tab, WebView view,
+            final HttpAuthHandler handler, final String host,
+            final String realm) {
+        String username = null;
+        String password = null;
+
+        boolean reuseHttpAuthUsernamePassword
+                = handler.useHttpAuthUsernamePassword();
+
+        if (reuseHttpAuthUsernamePassword && view != null) {
+            String[] credentials = view.getHttpAuthUsernamePassword(host, realm);
+            if (credentials != null && credentials.length == 2) {
+                username = credentials[0];
+                password = credentials[1];
+            }
+        }
+
+        if (username != null && password != null) {
+            handler.proceed(username, password);
+        } else {
+            if (tab.inForeground() /*&& !handler.suppressDialog()*/) {
+                mPageDialogsHandler.showHttpAuthentication(tab, handler, host, realm);
+            } else {
+                handler.cancel();
+            }
+        }
+    }
+
+    @Override
+    public void onDownloadStart(Tab tab, String url, String userAgent,
+            String contentDisposition, String mimetype, String referer,
+            long contentLength) {
+        WebView w = tab.getWebView();
+        boolean ret = DownloadHandler.onDownloadStart(mActivity, url, userAgent,
+                contentDisposition, mimetype, referer, w.isPrivateBrowsingEnabled(), contentLength);
+        if (ret == false && w.copyBackForwardList().getSize() == 0) {
+            // This Tab was opened for the sole purpose of downloading a
+            // file. Remove it.
+            if (tab == mTabControl.getCurrentTab()) {
+                // In this case, the Tab is still on top.
+                goBackOnePageOrQuit();
+            } else {
+                // In this case, it is not.
+                closeTab(tab);
+            }
+        }
+    }
+
+    @Override
+    public Bitmap getDefaultVideoPoster() {
+        return mUi.getDefaultVideoPoster();
+    }
+
+    @Override
+    public View getVideoLoadingProgressView() {
+        return mUi.getVideoLoadingProgressView();
+    }
+
+    @Override
+    public void showSslCertificateOnError(WebView view, SslErrorHandler handler,
+            SslError error) {
+        mPageDialogsHandler.showSSLCertificateOnError(view, handler, error);
+    }
+
+    @Override
+    public void showAutoLogin(Tab tab) {
+        assert tab.inForeground();
+        // Update the title bar to show the auto-login request.
+        mUi.showAutoLogin(tab);
+    }
+
+    @Override
+    public void hideAutoLogin(Tab tab) {
+        assert tab.inForeground();
+        mUi.hideAutoLogin(tab);
+    }
+
+    // helper method
+
+    /*
+     * Update the favorites icon if the private browsing isn't enabled and the
+     * icon is valid.
+     */
+    private void maybeUpdateFavicon(Tab tab, final String originalUrl,
+            final String url, Bitmap favicon) {
+        if (favicon == null) {
+            return;
+        }
+        if (!tab.isPrivateBrowsingEnabled()) {
+            Bookmarks.updateFavicon(mActivity
+                    .getContentResolver(), originalUrl, url, favicon);
+        }
+    }
+
+    @Override
+    public void bookmarkedStatusHasChanged(Tab tab) {
+        // TODO: Switch to using onTabDataChanged after b/3262950 is fixed
+        mUi.bookmarkedStatusHasChanged(tab);
+    }
+
+    // end WebViewController
+
+    protected void pageUp() {
+        getCurrentTopWebView().pageUp(false);
+    }
+
+    protected void pageDown() {
+        getCurrentTopWebView().pageDown(false);
+    }
+
+    // callback from phone title bar
+    @Override
+    public void editUrl() {
+        if (mOptionsMenuOpen) mActivity.closeOptionsMenu();
+        mUi.editUrl(false, true);
+    }
+
+    @Override
+    public void showCustomView(Tab tab, View view, int requestedOrientation,
+            CustomViewCallback callback) {
+        if (tab.inForeground()) {
+            if (mUi.isCustomViewShowing()) {
+                callback.onCustomViewHidden();
+                return;
+            }
+            mUi.showCustomView(view, requestedOrientation, callback);
+            // Save the menu state and set it to empty while the custom
+            // view is showing.
+            mOldMenuState = mMenuState;
+            mMenuState = EMPTY_MENU;
+            mActivity.invalidateOptionsMenu();
+        }
+    }
+
+    @Override
+    public void hideCustomView() {
+        if (mUi.isCustomViewShowing()) {
+            mUi.onHideCustomView();
+            // Reset the old menu state.
+            mMenuState = mOldMenuState;
+            mOldMenuState = EMPTY_MENU;
+            mActivity.invalidateOptionsMenu();
+        }
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode,
+            Intent intent) {
+        if (getCurrentTopWebView() == null) return;
+        switch (requestCode) {
+            case PREFERENCES_PAGE:
+                if (resultCode == Activity.RESULT_OK && intent != null) {
+                    String action = intent.getStringExtra(Intent.EXTRA_TEXT);
+                    if (PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY.equals(action)) {
+                        mTabControl.removeParentChildRelationShips();
+                    }
+                }
+                break;
+            case FILE_SELECTED:
+                // Chose a file from the file picker.
+                if (null == mUploadHandler) break;
+                mUploadHandler.onResult(resultCode, intent);
+                break;
+            case AUTOFILL_SETUP:
+                // Determine whether a profile was actually set up or not
+                // and if so, send the message back to the WebTextView to
+                // fill the form with the new profile.
+                if (getSettings().getAutoFillProfile() != null) {
+                    mAutoFillSetupMessage.sendToTarget();
+                    mAutoFillSetupMessage = null;
+                }
+                break;
+            case COMBO_VIEW:
+                if (intent == null || resultCode != Activity.RESULT_OK) {
+                    break;
+                }
+                mUi.showWeb(false);
+                if (Intent.ACTION_VIEW.equals(intent.getAction())) {
+                    Tab t = getCurrentTab();
+                    Uri uri = intent.getData();
+                    mUpdateMyNavThumbnail = true;
+                    mUpdateMyNavThumbnailUrl = uri.toString();
+                    loadUrl(t, uri.toString());
+                } else if (intent.hasExtra(ComboViewActivity.EXTRA_OPEN_ALL)) {
+                    String[] urls = intent.getStringArrayExtra(
+                            ComboViewActivity.EXTRA_OPEN_ALL);
+                    Tab parent = getCurrentTab();
+                    for (String url : urls) {
+                        if (url != null) {
+                            parent = openTab(url, parent,
+                                    !mSettings.openInBackground(), true);
+                        }
+                    }
+                } else if (intent.hasExtra(ComboViewActivity.EXTRA_OPEN_SNAPSHOT)) {
+                    long id = intent.getLongExtra(
+                            ComboViewActivity.EXTRA_OPEN_SNAPSHOT, -1);
+                    if (id >= 0) {
+                        createNewSnapshotTab(id, true);
+                    }
+                }
+                break;
+            case VOICE_RESULT:
+                if (resultCode == Activity.RESULT_OK && intent != null) {
+                    ArrayList<String> results = intent.getStringArrayListExtra(
+                            RecognizerIntent.EXTRA_RESULTS);
+                    if (results.size() >= 1) {
+                        mVoiceResult = results.get(0);
+                    }
+                }
+                break;
+             case MY_NAVIGATION:
+                if (intent == null || resultCode != Activity.RESULT_OK) {
+                    break;
+                }
+
+                if (intent.getBooleanExtra("need_refresh", false) &&
+                        getCurrentTopWebView() != null) {
+                    getCurrentTopWebView().reload();
+                }
+                break;
+            default:
+                break;
+        }
+        getCurrentTopWebView().requestFocus();
+    }
+
+    /**
+     * Open the Go page.
+     * @param startWithHistory If true, open starting on the history tab.
+     *                         Otherwise, start with the bookmarks tab.
+     */
+    @Override
+    public void bookmarksOrHistoryPicker(ComboViews startView) {
+        if (mTabControl.getCurrentWebView() == null) {
+            return;
+        }
+        // clear action mode
+        if (isInCustomActionMode()) {
+            endActionMode();
+        }
+        Bundle extras = new Bundle();
+        // Disable opening in a new window if we have maxed out the windows
+        extras.putBoolean(BrowserBookmarksPage.EXTRA_DISABLE_WINDOW,
+                !mTabControl.canCreateNewTab());
+        mUi.showComboView(startView, extras);
+    }
+
+    // combo view callbacks
+
+    // key handling
+    protected void onBackKey() {
+        if (!mUi.onBackKey()) {
+            WebView subwindow = mTabControl.getCurrentSubWindow();
+            if (subwindow != null) {
+                if (subwindow.canGoBack()) {
+                    subwindow.goBack();
+                } else {
+                    dismissSubWindow(mTabControl.getCurrentTab());
+                }
+            } else {
+                goBackOnePageOrQuit();
+            }
+        }
+    }
+
+    protected boolean onMenuKey() {
+        return mUi.onMenuKey();
+    }
+
+    // menu handling and state
+    // TODO: maybe put into separate handler
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        if (mMenuState == EMPTY_MENU) {
+            return false;
+        }
+        MenuInflater inflater = mActivity.getMenuInflater();
+        inflater.inflate(R.menu.browser, menu);
+        return true;
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v,
+            ContextMenuInfo menuInfo) {
+        if (v instanceof TitleBar) {
+            return;
+        }
+        if (!(v instanceof WebView)) {
+            return;
+        }
+        final WebView webview = (WebView) v;
+        WebView.HitTestResult result = webview.getHitTestResult();
+        if (result == null) {
+            return;
+        }
+
+        int type = result.getType();
+        if (type == WebView.HitTestResult.UNKNOWN_TYPE) {
+            Log.w(LOGTAG,
+                    "We should not show context menu when nothing is touched");
+            return;
+        }
+        if (type == WebView.HitTestResult.EDIT_TEXT_TYPE) {
+            // let TextView handles context menu
+            return;
+        }
+
+        // Note, http://b/issue?id=1106666 is requesting that
+        // an inflated menu can be used again. This is not available
+        // yet, so inflate each time (yuk!)
+        MenuInflater inflater = mActivity.getMenuInflater();
+        inflater.inflate(R.menu.browsercontext, menu);
+
+        // Show the correct menu group
+        final String extra = result.getExtra();
+        final String navigationUrl = MyNavigationUtil.getMyNavigationUrl(extra);
+        if (extra == null) return;
+        menu.setGroupVisible(R.id.PHONE_MENU,
+                type == WebView.HitTestResult.PHONE_TYPE);
+        menu.setGroupVisible(R.id.EMAIL_MENU,
+                type == WebView.HitTestResult.EMAIL_TYPE);
+        menu.setGroupVisible(R.id.GEO_MENU,
+                type == WebView.HitTestResult.GEO_TYPE);
+        String itemUrl = null;
+        String url = webview.getOriginalUrl();
+        if (url != null && url.equalsIgnoreCase(MyNavigationUtil.MY_NAVIGATION)) {
+            itemUrl = Uri.decode(navigationUrl);
+            if (itemUrl != null && !MyNavigationUtil.isDefaultMyNavigation(itemUrl)) {
+                menu.setGroupVisible(R.id.MY_NAVIGATION_MENU,
+                        type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+            } else {
+                menu.setGroupVisible(R.id.MY_NAVIGATION_MENU, false);
+            }
+            menu.setGroupVisible(R.id.IMAGE_MENU, false);
+            menu.setGroupVisible(R.id.ANCHOR_MENU, false);
+        } else {
+            menu.setGroupVisible(R.id.MY_NAVIGATION_MENU, false);
+
+            menu.setGroupVisible(R.id.IMAGE_MENU,
+                    type == WebView.HitTestResult.IMAGE_TYPE
+                            || type == WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE);
+            menu.setGroupVisible(R.id.ANCHOR_MENU,
+                    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(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_OR_EDIT);
+                addIntent.putExtra(Insert.PHONE, Uri.decode(extra));
+                addIntent.setType(ContactsContract.Contacts.CONTENT_ITEM_TYPE);
+                menu.findItem(R.id.add_contact_context_menu_id).setIntent(
+                        addIntent);
+                menu.findItem(R.id.copy_phone_context_menu_id)
+                        .setOnMenuItemClickListener(
+                        new Copy(extra));
+                break;
+
+            case WebView.HitTestResult.EMAIL_TYPE:
+                menu.setHeaderTitle(extra);
+                menu.findItem(R.id.email_context_menu_id).setIntent(
+                        new Intent(Intent.ACTION_VIEW, Uri
+                                .parse(WebView.SCHEME_MAILTO + extra)));
+                menu.findItem(R.id.copy_mail_context_menu_id)
+                        .setOnMenuItemClickListener(
+                        new Copy(extra));
+                break;
+
+            case WebView.HitTestResult.GEO_TYPE:
+                menu.setHeaderTitle(extra);
+                menu.findItem(R.id.map_context_menu_id).setIntent(
+                        new Intent(Intent.ACTION_VIEW, Uri
+                                .parse(WebView.SCHEME_GEO
+                                        + URLEncoder.encode(extra))));
+                menu.findItem(R.id.copy_geo_context_menu_id)
+                        .setOnMenuItemClickListener(
+                        new Copy(extra));
+                break;
+
+            case WebView.HitTestResult.SRC_ANCHOR_TYPE:
+            case WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE:
+                menu.setHeaderTitle(extra);
+                // decide whether to show the open link in new tab option
+                boolean showNewTab = mTabControl.canCreateNewTab();
+                MenuItem newTabItem
+                        = menu.findItem(R.id.open_newtab_context_menu_id);
+                newTabItem.setTitle(getSettings().openInBackground()
+                        ? R.string.contextmenu_openlink_newwindow_background
+                        : R.string.contextmenu_openlink_newwindow);
+                newTabItem.setVisible(showNewTab);
+                if (showNewTab) {
+                    if (WebView.HitTestResult.SRC_IMAGE_ANCHOR_TYPE == type) {
+                        newTabItem.setOnMenuItemClickListener(
+                                new MenuItem.OnMenuItemClickListener() {
+                                    @Override
+                                    public boolean onMenuItemClick(MenuItem item) {
+                                        final HashMap<String, WebView> hrefMap =
+                                                new HashMap<String, WebView>();
+                                        hrefMap.put("webview", webview);
+                                        final Message msg = mHandler.obtainMessage(
+                                                FOCUS_NODE_HREF,
+                                                R.id.open_newtab_context_menu_id,
+                                                0, hrefMap);
+                                        webview.requestFocusNodeHref(msg);
+                                        return true;
+                                    }
+                                });
+                    } else {
+                        newTabItem.setOnMenuItemClickListener(
+                                new MenuItem.OnMenuItemClickListener() {
+                                    @Override
+                                    public boolean onMenuItemClick(MenuItem item) {
+                                        final Tab parent = mTabControl.getCurrentTab();
+                                        openTab(extra, parent,
+                                                !mSettings.openInBackground(),
+                                                true);
+                                        return true;
+                                    }
+                                });
+                    }
+                }
+                if (url != null && url.equalsIgnoreCase(MyNavigationUtil.MY_NAVIGATION)) {
+                    menu.setHeaderTitle(navigationUrl);
+                    menu.findItem(R.id.open_newtab_context_menu_id).setVisible(false);
+
+                    if (itemUrl != null) {
+                        if (!MyNavigationUtil.isDefaultMyNavigation(itemUrl)) {
+                            menu.findItem(R.id.edit_my_navigation_context_menu_id)
+                                    .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                                        @Override
+                                        public boolean onMenuItemClick(MenuItem item) {
+                                            final Intent intent = new Intent(Controller.this
+                                                    .getContext(),
+                                                    AddMyNavigationPage.class);
+                                            Bundle bundle = new Bundle();
+                                            String url = Uri.decode(navigationUrl);
+                                            bundle.putBoolean("isAdding", false);
+                                            bundle.putString("url", url);
+                                            bundle.putString("name", getNameFromUrl(url));
+                                            intent.putExtra("websites", bundle);
+                                            mActivity.startActivityForResult(intent, MY_NAVIGATION);
+                                            return false;
+                                        }
+                                    });
+                            menu.findItem(R.id.delete_my_navigation_context_menu_id)
+                                    .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                                        @Override
+                                        public boolean onMenuItemClick(MenuItem item) {
+                                            showMyNavigationDeleteDialog(Uri.decode(navigationUrl));
+                                            return false;
+                                        }
+                                    });
+                        }
+                    } else {
+                        Log.e(LOGTAG, "mynavigation onCreateContextMenu itemUrl is null!");
+                    }
+                }
+                if (type == WebView.HitTestResult.SRC_ANCHOR_TYPE) {
+                    break;
+                }
+                // otherwise fall through to handle image part
+            case WebView.HitTestResult.IMAGE_TYPE:
+                MenuItem shareItem = menu.findItem(R.id.share_link_context_menu_id);
+                shareItem.setVisible(type == WebView.HitTestResult.IMAGE_TYPE);
+                if (type == WebView.HitTestResult.IMAGE_TYPE) {
+                    menu.setHeaderTitle(extra);
+                    shareItem.setOnMenuItemClickListener(
+                            new MenuItem.OnMenuItemClickListener() {
+                                @Override
+                                public boolean onMenuItemClick(MenuItem item) {
+                                    sharePage(mActivity, null, extra, null,
+                                    null);
+                                    return true;
+                                }
+                            }
+                        );
+                }
+                menu.findItem(R.id.view_image_context_menu_id)
+                        .setOnMenuItemClickListener(new OnMenuItemClickListener() {
+                    @Override
+                    public boolean onMenuItemClick(MenuItem item) {
+                        openTab(extra, mTabControl.getCurrentTab(), true, true);
+                        return false;
+                    }
+                });
+                menu.findItem(R.id.download_context_menu_id).setOnMenuItemClickListener(
+                        new Download(mActivity, extra, webview.isPrivateBrowsingEnabled(),
+                                webview.getSettings().getUserAgentString()));
+                menu.findItem(R.id.set_wallpaper_context_menu_id).
+                        setOnMenuItemClickListener(new WallpaperHandler(mActivity,
+                                extra));
+                break;
+
+            default:
+                Log.w(LOGTAG, "We should not get here.");
+                break;
+        }
+        //update the ui
+        mUi.onContextMenuCreated(menu);
+    }
+
+    public void startAddMyNavigation(String url) {
+        final Intent intent = new Intent(Controller.this.getContext(), AddMyNavigationPage.class);
+        Bundle bundle = new Bundle();
+        bundle.putBoolean("isAdding", true);
+        bundle.putString("url", url);
+        bundle.putString("name", getNameFromUrl(url));
+        intent.putExtra("websites", bundle);
+        mActivity.startActivityForResult(intent, MY_NAVIGATION);
+    }
+
+    private void showMyNavigationDeleteDialog(final String itemUrl) {
+        new AlertDialog.Builder(this.getContext())
+                .setTitle(R.string.my_navigation_delete_label)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setMessage(R.string.my_navigation_delete_msg)
+                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        deleteMyNavigationItem(itemUrl);
+                    }
+                })
+                .setNegativeButton(R.string.cancel, null)
+                .show();
+    }
+
+    private void deleteMyNavigationItem(final String itemUrl) {
+        ContentResolver cr = this.getContext().getContentResolver();
+        Cursor cursor = null;
+
+        try {
+            cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                    new String[] {
+                        MyNavigationUtil.ID
+                    }, "url = ?", new String[] {
+                        itemUrl
+                    }, null);
+            if (null != cursor && cursor.moveToFirst()) {
+                Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+                        cursor.getLong(0));
+
+                ContentValues values = new ContentValues();
+                values.put(MyNavigationUtil.TITLE, "");
+                values.put(MyNavigationUtil.URL, "ae://" + cursor.getLong(0) + "add-fav");
+                values.put(MyNavigationUtil.WEBSITE, 0 + "");
+                ByteArrayOutputStream os = new ByteArrayOutputStream();
+                Bitmap bm = BitmapFactory.decodeResource(this.getContext().getResources(),
+                        R.raw.my_navigation_add);
+                bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                Log.d(LOGTAG, "deleteMyNavigationItem uri is : " + uri);
+                cr.update(uri, values, null, null);
+            } else {
+                Log.e(LOGTAG, "deleteMyNavigationItem the item does not exist!");
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "deleteMyNavigationItem", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+
+        if (getCurrentTopWebView() != null) {
+            getCurrentTopWebView().reload();
+        }
+    }
+
+    private String getNameFromUrl(String itemUrl) {
+        ContentResolver cr = this.getContext().getContentResolver();
+        Cursor cursor = null;
+        String name = null;
+
+        try {
+            cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                    new String[] {
+                        MyNavigationUtil.TITLE
+                    }, "url = ?", new String[] {
+                        itemUrl
+                    }, null);
+            if (null != cursor && cursor.moveToFirst()) {
+                name = cursor.getString(0);
+            } else {
+                Log.e(LOGTAG, "this item does not exist!");
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "getNameFromUrl", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+        return name;
+    }
+
+    private void updateMyNavigationThumbnail(final String itemUrl, WebView webView) {
+        int width = mActivity.getResources().getDimensionPixelOffset(
+                R.dimen.myNavigationThumbnailWidth);
+        int height = mActivity.getResources().getDimensionPixelOffset(
+                R.dimen.myNavigationThumbnailHeight);
+
+        final Bitmap bm = createScreenshot(webView, width, height);
+
+        if (bm == null) {
+            Log.e(LOGTAG, "updateMyNavigationThumbnail bm is null!");
+            return;
+        }
+
+        final ContentResolver cr = mActivity.getContentResolver();
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... unused) {
+                ContentResolver cr = mActivity.getContentResolver();
+                Cursor cursor = null;
+                try {
+                    cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                            new String[] {
+                                MyNavigationUtil.ID
+                            }, "url = ?", new String[] {
+                                itemUrl
+                            }, null);
+                    if (null != cursor && cursor.moveToFirst()) {
+                        final ByteArrayOutputStream os = new ByteArrayOutputStream();
+                        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+                        ContentValues values = new ContentValues();
+                        values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                        Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+                                cursor.getLong(0));
+                        Log.d(LOGTAG, "updateMyNavigationThumbnail uri is " + uri);
+                        cr.update(uri, values, null, null);
+                        os.close();
+                    }
+                } catch (IllegalStateException e) {
+                    Log.e(LOGTAG, "updateMyNavigationThumbnail", e);
+                } catch (IOException e) {
+                    Log.e(LOGTAG, "updateMyNavigationThumbnail", e);
+                } finally {
+                    if (null != cursor) {
+                        cursor.close();
+                    }
+                }
+                return null;
+            }
+        }.execute();
+    }
+    /**
+     * As the menu can be open when loading state changes
+     * we must manually update the state of the stop/reload menu
+     * item
+     */
+    private void updateInLoadMenuItems(Menu menu, Tab tab) {
+        if (menu == null) {
+            return;
+        }
+        MenuItem dest = menu.findItem(R.id.stop_reload_menu_id);
+        MenuItem src = ((tab != null) && tab.inPageLoad()) ?
+                menu.findItem(R.id.stop_menu_id):
+                menu.findItem(R.id.reload_menu_id);
+        if (src != null) {
+            dest.setIcon(src.getIcon());
+            dest.setTitle(src.getTitle());
+        }
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        updateInLoadMenuItems(menu, getCurrentTab());
+        // hold on to the menu reference here; it is used by the page callbacks
+        // to update the menu based on loading state
+        mCachedMenu = menu;
+        // Note: setVisible will decide whether an item is visible; while
+        // setEnabled() will decide whether an item is enabled, which also means
+        // whether the matching shortcut key will function.
+        switch (mMenuState) {
+            case EMPTY_MENU:
+                if (mCurrentMenuState != mMenuState) {
+                    menu.setGroupVisible(R.id.MAIN_MENU, false);
+                    menu.setGroupEnabled(R.id.MAIN_MENU, false);
+                    menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, false);
+                }
+                break;
+            default:
+                if (mCurrentMenuState != mMenuState) {
+                    menu.setGroupVisible(R.id.MAIN_MENU, true);
+                    menu.setGroupEnabled(R.id.MAIN_MENU, true);
+                    menu.setGroupEnabled(R.id.MAIN_SHORTCUT_MENU, true);
+                }
+                updateMenuState(getCurrentTab(), menu);
+                break;
+        }
+        mCurrentMenuState = mMenuState;
+        return mUi.onPrepareOptionsMenu(menu);
+    }
+
+    @Override
+    public void updateMenuState(Tab tab, Menu menu) {
+        boolean canGoBack = false;
+        boolean canGoForward = false;
+        boolean isDesktopUa = false;
+        boolean isLive = false;
+        if (tab != null) {
+            canGoBack = tab.canGoBack();
+            canGoForward = tab.canGoForward();
+            isDesktopUa = mSettings.hasDesktopUseragent(tab.getWebView());
+            isLive = !tab.isSnapshot();
+        }
+        final MenuItem back = menu.findItem(R.id.back_menu_id);
+        back.setEnabled(canGoBack);
+
+        final MenuItem home = menu.findItem(R.id.homepage_menu_id);
+
+        final MenuItem forward = menu.findItem(R.id.forward_menu_id);
+        forward.setEnabled(canGoForward);
+
+        final MenuItem source = menu.findItem(isInLoad() ? R.id.stop_menu_id
+                : R.id.reload_menu_id);
+        final MenuItem dest = menu.findItem(R.id.stop_reload_menu_id);
+        if (source != null && dest != null) {
+            dest.setTitle(source.getTitle());
+            dest.setIcon(source.getIcon());
+        }
+        menu.setGroupVisible(R.id.NAV_MENU, isLive);
+
+        // decide whether to show the share link option
+        PackageManager pm = mActivity.getPackageManager();
+        Intent send = new Intent(Intent.ACTION_SEND);
+        send.setType("text/plain");
+        ResolveInfo ri = pm.resolveActivity(send,
+                PackageManager.MATCH_DEFAULT_ONLY);
+        menu.findItem(R.id.share_page_menu_id).setVisible(ri != null);
+
+        boolean isNavDump = mSettings.enableNavDump();
+        final MenuItem nav = menu.findItem(R.id.dump_nav_menu_id);
+        nav.setVisible(isNavDump);
+        nav.setEnabled(isNavDump);
+
+        boolean showDebugSettings = mSettings.isDebugEnabled();
+        final MenuItem uaSwitcher = menu.findItem(R.id.ua_desktop_menu_id);
+        uaSwitcher.setChecked(isDesktopUa);
+        menu.setGroupVisible(R.id.LIVE_MENU, isLive);
+        menu.setGroupVisible(R.id.SNAPSHOT_MENU, !isLive);
+        // history and snapshots item are the members of COMBO menu group,
+        // so if show history item, only make snapshots item invisible.
+        menu.findItem(R.id.snapshots_menu_id).setVisible(false);
+
+        mUi.updateMenuState(tab, menu);
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (null == getCurrentTopWebView()) {
+            return false;
+        }
+        if (mMenuIsDown) {
+            // The shortcut action consumes the MENU. Even if it is still down,
+            // it won't trigger the next shortcut action. In the case of the
+            // shortcut action triggering a new activity, like Bookmarks, we
+            // won't get onKeyUp for MENU. So it is important to reset it here.
+            mMenuIsDown = false;
+        }
+        if (mUi.onOptionsItemSelected(item)) {
+            // ui callback handled it
+            return true;
+        }
+        switch (item.getItemId()) {
+            // -- Main menu
+            case R.id.new_tab_menu_id:
+                openTabToHomePage();
+                break;
+
+            case R.id.incognito_menu_id:
+                openIncognitoTab();
+                break;
+
+            case R.id.close_other_tabs_id:
+                closeOtherTabs();
+                break;
+
+            case R.id.goto_menu_id:
+                editUrl();
+                break;
+
+            case R.id.bookmarks_menu_id:
+                bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+                break;
+
+            case R.id.history_menu_id:
+                bookmarksOrHistoryPicker(ComboViews.History);
+                break;
+
+            case R.id.snapshots_menu_id:
+                bookmarksOrHistoryPicker(ComboViews.Snapshots);
+                break;
+
+            case R.id.add_bookmark_menu_id:
+                bookmarkCurrentPage();
+                break;
+
+            case R.id.stop_reload_menu_id:
+                if (isInLoad()) {
+                    stopLoading();
+                } else {
+                    Tab currentTab = mTabControl.getCurrentTab();
+                    if (currentTab.hasCrashed) {
+                        currentTab.replaceCrashView(getCurrentTopWebView(),
+                            currentTab.getViewContainer());
+                    }
+                    getCurrentTopWebView().reload();
+                }
+                break;
+
+            case R.id.back_menu_id:
+                getCurrentTab().goBack();
+                break;
+
+            case R.id.forward_menu_id:
+                getCurrentTab().goForward();
+                break;
+
+            case R.id.close_menu_id:
+                // Close the subwindow if it exists.
+                if (mTabControl.getCurrentSubWindow() != null) {
+                    dismissSubWindow(mTabControl.getCurrentTab());
+                    break;
+                }
+                closeCurrentTab();
+                break;
+
+            case R.id.exit_menu_id:
+                Object[] params  = { new String("persist.debug.browsermonkeytest")};
+                Class[] type = new Class[] {String.class};
+                String ret = (String)ReflectHelper.invokeStaticMethod(
+                             "android.os.SystemProperties","get", type, params);
+                if (ret != null && ret.equals("enable"))
+                    break;
+                showExitDialog(mActivity);
+                return true;
+            case R.id.homepage_menu_id:
+                Tab current = mTabControl.getCurrentTab();
+                loadUrl(current, mSettings.getHomePage());
+                break;
+
+            case R.id.preferences_menu_id:
+                openPreferences();
+                break;
+
+            case R.id.find_menu_id:
+                findOnPage();
+                break;
+
+            case R.id.save_snapshot_menu_id:
+                final Tab source = getTabControl().getCurrentTab();
+                if (source == null) break;
+                new SaveSnapshotTask(source).execute();
+                break;
+
+            case R.id.page_info_menu_id:
+                showPageInfo();
+                break;
+
+            case R.id.snapshot_go_live:
+                goLive();
+                return true;
+
+            case R.id.share_page_menu_id:
+                Tab currentTab = mTabControl.getCurrentTab();
+                if (null == currentTab) {
+                    return false;
+                }
+                shareCurrentPage(currentTab);
+                break;
+
+            case R.id.dump_nav_menu_id:
+                getCurrentTopWebView().debugDump();
+                break;
+
+            case R.id.zoom_in_menu_id:
+                getCurrentTopWebView().zoomIn();
+                break;
+
+            case R.id.zoom_out_menu_id:
+                getCurrentTopWebView().zoomOut();
+                break;
+
+            case R.id.view_downloads_menu_id:
+                viewDownloads();
+                break;
+
+            case R.id.ua_desktop_menu_id:
+                toggleUserAgent();
+                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) {
+                            Tab desiredTab = mTabControl.getTab(id);
+                            if (desiredTab != null &&
+                                    desiredTab != mTabControl.getCurrentTab()) {
+                                switchToTab(desiredTab);
+                            }
+                            break;
+                        }
+                    }
+                }
+                break;
+
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    private class SaveSnapshotTask extends AsyncTask<Void, Void, Long>
+            implements OnCancelListener {
+
+        private Tab mTab;
+        private Dialog mProgressDialog;
+        private ContentValues mValues;
+
+        private SaveSnapshotTask(Tab tab) {
+            mTab = tab;
+        }
+
+        @Override
+        protected void onPreExecute() {
+            CharSequence message = mActivity.getText(R.string.saving_snapshot);
+            mProgressDialog = ProgressDialog.show(mActivity, null, message,
+                    true, true, this);
+            mValues = mTab.createSnapshotValues();
+        }
+
+        @Override
+        protected Long doInBackground(Void... params) {
+            if (!mTab.saveViewState(mValues)) {
+                return null;
+            }
+            if (isCancelled()) {
+                String path = mValues.getAsString(Snapshots.VIEWSTATE_PATH);
+                File file = mActivity.getFileStreamPath(path);
+                if (!file.delete()) {
+                    file.deleteOnExit();
+                }
+                return null;
+            }
+            final ContentResolver cr = mActivity.getContentResolver();
+            Uri result = cr.insert(Snapshots.CONTENT_URI, mValues);
+            if (result == null) {
+                return null;
+            }
+            long id = ContentUris.parseId(result);
+            return id;
+        }
+
+        @Override
+        protected void onPostExecute(Long id) {
+            if (isCancelled()) {
+                return;
+            }
+            mProgressDialog.dismiss();
+            if (id == null) {
+                Toast.makeText(mActivity, R.string.snapshot_failed,
+                        Toast.LENGTH_SHORT).show();
+                return;
+            }
+            Bundle b = new Bundle();
+            b.putLong(BrowserSnapshotPage.EXTRA_ANIMATE_ID, id);
+            mUi.showComboView(ComboViews.Snapshots, b);
+        }
+
+        @Override
+        public void onCancel(DialogInterface dialog) {
+            cancel(true);
+        }
+    }
+
+    @Override
+    public void toggleUserAgent() {
+        WebView web = getCurrentWebView();
+        mSettings.toggleDesktopUseragent(web);
+        web.loadUrl(web.getOriginalUrl());
+    }
+
+    @Override
+    public void findOnPage() {
+        getCurrentTopWebView().showFindDialog(null, true);
+    }
+
+    @Override
+    public void openPreferences() {
+        Intent intent = new Intent(mActivity, BrowserPreferencesPage.class);
+        intent.putExtra(BrowserPreferencesPage.CURRENT_PAGE,
+                getCurrentTopWebView().getUrl());
+        mActivity.startActivityForResult(intent, PREFERENCES_PAGE);
+    }
+
+    @Override
+    public void bookmarkCurrentPage() {
+        Intent bookmarkIntent = createBookmarkCurrentPageIntent(false);
+        if (bookmarkIntent != null) {
+            mActivity.startActivity(bookmarkIntent);
+        }
+    }
+
+    private void goLive() {
+        SnapshotTab t = (SnapshotTab) getCurrentTab();
+        t.loadUrl(t.getLiveUrl(), null);
+    }
+
+    private void showExitDialog(final Activity activity) {
+        new AlertDialog.Builder(activity)
+                .setTitle(R.string.exit_browser_title)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setMessage(R.string.exit_browser_msg)
+                .setNegativeButton(R.string.exit_minimize, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        activity.moveTaskToBack(true);
+                        dialog.dismiss();
+                    }
+                })
+                .setPositiveButton(R.string.exit_quit, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        activity.finish();
+                        mHandler.postDelayed(new Runnable() {
+                            @Override
+                            public void run() {
+                                // TODO Auto-generated method stub
+                                mCrashRecoveryHandler.clearState(true);
+                                int pid = android.os.Process.myPid();
+                                android.os.Process.killProcess(pid);
+                            }
+                        }, 300);
+                        dialog.dismiss();
+                    }
+                })
+                .show();
+    }
+    @Override
+    public void showPageInfo() {
+        mPageDialogsHandler.showPageInfo(mTabControl.getCurrentTab(), false, null);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        // Let the History and Bookmark fragments handle menus they created.
+        if (item.getGroupId() == R.id.CONTEXT_MENU) {
+            return false;
+        }
+
+        int id = item.getItemId();
+        boolean result = true;
+        switch (id) {
+            // -- Browser context menu
+            case R.id.open_context_menu_id:
+            case R.id.save_link_context_menu_id:
+            case R.id.copy_link_context_menu_id:
+                final WebView webView = getCurrentTopWebView();
+                if (null == webView) {
+                    result = false;
+                    break;
+                }
+                final HashMap<String, WebView> hrefMap =
+                        new HashMap<String, WebView>();
+                hrefMap.put("webview", webView);
+                final Message msg = mHandler.obtainMessage(
+                        FOCUS_NODE_HREF, id, 0, hrefMap);
+                webView.requestFocusNodeHref(msg);
+                break;
+
+            default:
+                // For other context menus
+                result = onOptionsItemSelected(item);
+        }
+        return result;
+    }
+
+    /**
+     * support programmatically opening the context menu
+     */
+    public void openContextMenu(View view) {
+        mActivity.openContextMenu(view);
+    }
+
+    /**
+     * programmatically open the options menu
+     */
+    public void openOptionsMenu() {
+        mActivity.openOptionsMenu();
+    }
+
+    @Override
+    public boolean onMenuOpened(int featureId, Menu menu) {
+        if (mOptionsMenuOpen) {
+            if (mConfigChanged) {
+                // We do not need to make any changes to the state of the
+                // title bar, since the only thing that happened was a
+                // change in orientation
+                mConfigChanged = false;
+            } else {
+                if (!mExtendedMenuOpen) {
+                    mExtendedMenuOpen = true;
+                    mUi.onExtendedMenuOpened();
+                } else {
+                    // Switching the menu back to icon view, so show the
+                    // title bar once again.
+                    mExtendedMenuOpen = false;
+                    mUi.onExtendedMenuClosed(isInLoad());
+                }
+            }
+        } else {
+            // The options menu is closed, so open it, and show the title
+            mOptionsMenuOpen = true;
+            mConfigChanged = false;
+            mExtendedMenuOpen = false;
+            mUi.onOptionsMenuOpened();
+        }
+        return true;
+    }
+
+    @Override
+    public void onOptionsMenuClosed(Menu menu) {
+        mOptionsMenuOpen = false;
+        mUi.onOptionsMenuClosed(isInLoad());
+    }
+
+    @Override
+    public void onContextMenuClosed(Menu menu) {
+        mUi.onContextMenuClosed(menu, isInLoad());
+    }
+
+    // Helper method for getting the top window.
+    @Override
+    public WebView getCurrentTopWebView() {
+        return mTabControl.getCurrentTopWebView();
+    }
+
+    @Override
+    public WebView getCurrentWebView() {
+        return mTabControl.getCurrentWebView();
+    }
+
+    /*
+     * This method is called as a result of the user selecting the options
+     * menu to see the download window. It shows the download window on top of
+     * the current window.
+     */
+    void viewDownloads() {
+        Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+        mActivity.startActivity(intent);
+    }
+
+    int getActionModeHeight() {
+        TypedArray actionBarSizeTypedArray = mActivity.obtainStyledAttributes(
+                    new int[] { android.R.attr.actionBarSize });
+        int size = (int) actionBarSizeTypedArray.getDimension(0, 0f);
+        actionBarSizeTypedArray.recycle();
+        return size;
+    }
+
+    // action mode
+
+    @Override
+    public void onActionModeStarted(ActionMode mode) {
+        mUi.onActionModeStarted(mode);
+        mActionMode = mode;
+    }
+
+    /*
+     * True if a custom ActionMode (i.e. find or select) is in use.
+     */
+    @Override
+    public boolean isInCustomActionMode() {
+        return mActionMode != null;
+    }
+
+    /*
+     * End the current ActionMode.
+     */
+    @Override
+    public void endActionMode() {
+        if (mActionMode != null) {
+            mActionMode.finish();
+        }
+    }
+
+    /*
+     * Called by find and select when they are finished.  Replace title bars
+     * as necessary.
+     */
+    @Override
+    public void onActionModeFinished(ActionMode mode) {
+        if (!isInCustomActionMode()) return;
+        mUi.onActionModeFinished(isInLoad());
+        mActionMode = null;
+    }
+
+    boolean isInLoad() {
+        final Tab tab = getCurrentTab();
+        return (tab != null) && tab.inPageLoad();
+    }
+
+    // bookmark handling
+
+    /**
+     * add the current page as a bookmark to the given folder id
+     * @param folderId use -1 for the default folder
+     * @param editExisting If true, check to see whether the site is already
+     *          bookmarked, and if it is, edit that bookmark.  If false, and
+     *          the site is already bookmarked, do not attempt to edit the
+     *          existing bookmark.
+     */
+    @Override
+    public Intent createBookmarkCurrentPageIntent(boolean editExisting) {
+        WebView w = getCurrentTopWebView();
+        if (w == null) {
+            return null;
+        }
+        Intent i = new Intent(mActivity,
+                AddBookmarkPage.class);
+        i.putExtra(BrowserContract.Bookmarks.URL, w.getUrl());
+        i.putExtra(BrowserContract.Bookmarks.TITLE, w.getTitle());
+        String touchIconUrl = w.getTouchIconUrl();
+        if (touchIconUrl != null) {
+            i.putExtra(AddBookmarkPage.TOUCH_ICON_URL, touchIconUrl);
+            WebSettings settings = w.getSettings();
+            if (settings != null) {
+                i.putExtra(AddBookmarkPage.USER_AGENT,
+                        settings.getUserAgentString());
+            }
+        }
+        i.putExtra(BrowserContract.Bookmarks.THUMBNAIL,
+                createScreenshot(w, getDesiredThumbnailWidth(mActivity),
+                getDesiredThumbnailHeight(mActivity)));
+        i.putExtra(BrowserContract.Bookmarks.FAVICON, w.getFavicon());
+        if (editExisting) {
+            i.putExtra(AddBookmarkPage.CHECK_FOR_DUPE, true);
+        }
+        // Put the dialog at the upper right of the screen, covering the
+        // star on the title bar.
+        i.putExtra("gravity", Gravity.RIGHT | Gravity.TOP);
+        return i;
+    }
+
+    // file chooser
+    @Override
+    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+        mUploadHandler = new UploadHandler(this);
+        mUploadHandler.openFileChooser(uploadMsg, acceptType, capture);
+    }
+
+    // thumbnails
+
+    /**
+     * Return the desired width for thumbnail screenshots, which are stored in
+     * the database, and used on the bookmarks screen.
+     * @param context Context for finding out the density of the screen.
+     * @return desired width for thumbnail screenshot.
+     */
+    static int getDesiredThumbnailWidth(Context context) {
+        return context.getResources().getDimensionPixelOffset(
+                R.dimen.bookmarkThumbnailWidth);
+    }
+
+    /**
+     * Return the desired height for thumbnail screenshots, which are stored in
+     * the database, and used on the bookmarks screen.
+     * @param context Context for finding out the density of the screen.
+     * @return desired height for thumbnail screenshot.
+     */
+    static int getDesiredThumbnailHeight(Context context) {
+        return context.getResources().getDimensionPixelOffset(
+                R.dimen.bookmarkThumbnailHeight);
+    }
+
+    static Bitmap createScreenshot(WebView view, int width, int height) {
+        if (view == null || width == 0 || height == 0) {
+            return null;
+        }
+
+        Bitmap viewportBitmap = view.getViewportBitmap();
+        if (viewportBitmap == null) {
+            return null;
+        }
+
+        float aspectRatio = (float) width/height;
+        int viewportWidth = viewportBitmap.getWidth();
+        int viewportHeight = viewportBitmap.getHeight();
+
+        //modify the size to attain the same aspect ratio of desired thumbnail size
+        if (viewportHeight > viewportWidth) {
+            viewportHeight= (int)Math.round(viewportWidth * aspectRatio);
+        } else {
+            viewportWidth = (int)Math.round(viewportHeight * aspectRatio);
+        }
+
+        Rect srcRect = new Rect(0, 0, viewportWidth, viewportHeight);
+        Rect dstRect = new Rect(0, 0, width, height);
+
+        if (sThumbnailBitmap == null || sThumbnailBitmap.getWidth() != width
+                   || sThumbnailBitmap.getHeight() != height) {
+            if (sThumbnailBitmap != null) {
+                sThumbnailBitmap.recycle();
+                sThumbnailBitmap = null;
+            }
+
+            sThumbnailBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+        }
+
+        Canvas canvas = new Canvas(sThumbnailBitmap);
+        canvas.drawBitmap(viewportBitmap, srcRect, dstRect, new Paint(Paint.FILTER_BITMAP_FLAG));
+
+        return sThumbnailBitmap;
+    }
+
+    private void updateScreenshot(Tab tab) {
+        // If this is a bookmarked site, add a screenshot to the database.
+        // FIXME: Would like to make sure there is actually something to
+        // draw, but the API for that (WebViewCore.pictureReady()) is not
+        // currently accessible here.
+
+        WebView view = tab.getWebView();
+        if (view == null) {
+            // Tab was destroyed
+            return;
+        }
+        final String url = tab.getUrl();
+        final String originalUrl = view.getOriginalUrl();
+        final String thumbnailUrl = mUpdateMyNavThumbnailUrl;
+        if (TextUtils.isEmpty(url)) {
+            return;
+        }
+
+        //update My Navigation Thumbnails
+        boolean isMyNavigationUrl = MyNavigationUtil.isMyNavigationUrl(mActivity, url);
+        if (isMyNavigationUrl) {
+            updateMyNavigationThumbnail(url, view);
+        }
+        // Only update thumbnails for web urls (http(s)://), not for
+        // about:, javascript:, data:, etc...
+        // Unless it is a bookmarked site, then always update
+        if (!Patterns.WEB_URL.matcher(url).matches() && !tab.isBookmarkedSite()) {
+            return;
+        }
+
+        if (url != null && mUpdateMyNavThumbnailUrl != null
+                && Patterns.WEB_URL.matcher(url).matches()
+                && Patterns.WEB_URL.matcher(mUpdateMyNavThumbnailUrl).matches()) {
+            String urlHost = (new WebAddress(url)).getHost();
+            String bookmarkHost = (new WebAddress(mUpdateMyNavThumbnailUrl)).getHost();
+            if (urlHost == null || urlHost.length() == 0 || bookmarkHost == null
+                    || bookmarkHost.length() == 0) {
+                return;
+            }
+            String urlDomain = urlHost.substring(urlHost.indexOf('.'), urlHost.length());
+            String bookmarkDomain = bookmarkHost.substring(bookmarkHost.indexOf('.'),
+                    bookmarkHost.length());
+            Log.d(LOGTAG, "addressUrl domain is  " + urlDomain);
+            Log.d(LOGTAG, "bookmarkUrl domain is " + bookmarkDomain);
+            if (!bookmarkDomain.equals(urlDomain)) {
+                return;
+            }
+        }
+        final Bitmap bm = createScreenshot(view, getDesiredThumbnailWidth(mActivity),
+                getDesiredThumbnailHeight(mActivity));
+        if (bm == null) {
+            if (!mHandler.hasMessages(UPDATE_BOOKMARK_THUMBNAIL, tab)) {
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(
+                     UPDATE_BOOKMARK_THUMBNAIL, 0, 0, tab),
+                     500);
+            }
+            return;
+        }
+
+        final ContentResolver cr = mActivity.getContentResolver();
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... unused) {
+                Cursor cursor = null;
+                try {
+                    // TODO: Clean this up
+                    cursor = Bookmarks.queryCombinedForUrl(cr, originalUrl,
+                            mUpdateMyNavThumbnail ? ((thumbnailUrl != null) ? thumbnailUrl : url)
+                                    : url);
+                    if (mUpdateMyNavThumbnail) {
+                        mUpdateMyNavThumbnail = false;
+                        mUpdateMyNavThumbnailUrl = null;
+                    }
+                    if (cursor != null && cursor.moveToFirst()) {
+                        final ByteArrayOutputStream os =
+                                new ByteArrayOutputStream();
+                        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+                        ContentValues values = new ContentValues();
+                        values.put(Images.THUMBNAIL, os.toByteArray());
+
+                        do {
+                            values.put(Images.URL, cursor.getString(0));
+                            cr.update(Images.CONTENT_URI, values, null, null);
+                        } while (cursor.moveToNext());
+                    }
+                } catch (IllegalStateException e) {
+                    // Ignore
+                } catch (SQLiteException s) {
+                    // Added for possible error when user tries to remove the same bookmark
+                    // that is being updated with a screen shot
+                    Log.w(LOGTAG, "Error when running updateScreenshot ", s);
+                } finally {
+                    if (cursor != null) cursor.close();
+                }
+                return null;
+            }
+        }.execute();
+    }
+
+    private class Copy implements OnMenuItemClickListener {
+        private CharSequence mText;
+
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            copy(mText);
+            return true;
+        }
+
+        public Copy(CharSequence toCopy) {
+            mText = toCopy;
+        }
+    }
+
+    private static class Download implements OnMenuItemClickListener {
+        private Activity mActivity;
+        private String mText;
+        private boolean mPrivateBrowsing;
+        private String mUserAgent;
+        private static final String FALLBACK_EXTENSION = "dat";
+        private static final String IMAGE_BASE_FORMAT = "yyyy-MM-dd-HH-mm-ss-";
+
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            if (DataUri.isDataUri(mText)) {
+                saveDataUri();
+            } else {
+                DownloadHandler.onDownloadStartNoStream(mActivity, mText, mUserAgent,
+                        null, null, null, mPrivateBrowsing, 0);
+            }
+            return true;
+        }
+
+        public Download(Activity activity, String toDownload, boolean privateBrowsing,
+                String userAgent) {
+            mActivity = activity;
+            mText = toDownload;
+            mPrivateBrowsing = privateBrowsing;
+            mUserAgent = userAgent;
+        }
+
+        /**
+         * Treats mText as a data URI and writes its contents to a file
+         * based on the current time.
+         */
+        private void saveDataUri() {
+            FileOutputStream outputStream = null;
+            try {
+                DataUri uri = new DataUri(mText);
+                File target = getTarget(uri);
+                outputStream = new FileOutputStream(target);
+                outputStream.write(uri.getData());
+                final DownloadManager manager =
+                        (DownloadManager) mActivity.getSystemService(Context.DOWNLOAD_SERVICE);
+                 manager.addCompletedDownload(target.getName(),
+                        mActivity.getTitle().toString(), false,
+                        uri.getMimeType(), target.getAbsolutePath(),
+                        uri.getData().length, true);
+            } catch (IOException e) {
+                Log.e(LOGTAG, "Could not save data URL");
+            } finally {
+                if (outputStream != null) {
+                    try {
+                        outputStream.close();
+                    } catch (IOException e) {
+                        // ignore close errors
+                    }
+                }
+            }
+        }
+
+        /**
+         * Creates a File based on the current time stamp and uses
+         * the mime type of the DataUri to get the extension.
+         */
+        private File getTarget(DataUri uri) throws IOException {
+            File dir = mActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS);
+            DateFormat format = new SimpleDateFormat(IMAGE_BASE_FORMAT, Locale.US);
+            String nameBase = format.format(new Date());
+            String mimeType = uri.getMimeType();
+            MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+            String extension = mimeTypeMap.getExtensionFromMimeType(mimeType);
+            if (extension == null) {
+                Log.w(LOGTAG, "Unknown mime type in data URI" + mimeType);
+                extension = FALLBACK_EXTENSION;
+            }
+            extension = "." + extension; // createTempFile needs the '.'
+            File targetFile = File.createTempFile(nameBase, extension, dir);
+            return targetFile;
+        }
+    }
+
+    private static class SelectText implements OnMenuItemClickListener {
+        private WebView mWebView;
+
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            if (mWebView != null) {
+                return mWebView.selectText();
+            }
+            return false;
+        }
+
+        public SelectText(WebView webView) {
+           mWebView = webView;
+        }
+
+    }
+
+    /********************** TODO: UI stuff *****************************/
+
+    // these methods have been copied, they still need to be cleaned up
+
+    /****************** tabs ***************************************************/
+
+    // basic tab interactions:
+
+    // it is assumed that tabcontrol already knows about the tab
+    protected void addTab(Tab tab) {
+        mUi.addTab(tab);
+    }
+
+    protected void removeTab(Tab tab) {
+        mUi.removeTab(tab);
+        mTabControl.removeTab(tab);
+        mCrashRecoveryHandler.backupState();
+    }
+
+    @Override
+    public void setActiveTab(Tab tab) {
+        // monkey protection against delayed start
+        if (tab != null) {
+            mTabControl.setCurrentTab(tab);
+            // the tab is guaranteed to have a webview after setCurrentTab
+            mUi.setActiveTab(tab);
+            tab.setTimeStamp();
+        }
+    }
+
+    protected void closeEmptyTab() {
+        Tab current = mTabControl.getCurrentTab();
+        if (current != null
+                && current.getWebView().copyBackForwardList().getSize() == 0) {
+            closeCurrentTab();
+        }
+    }
+
+    protected void reuseTab(Tab appTab, UrlData urlData) {
+        // Dismiss the subwindow if applicable.
+        dismissSubWindow(appTab);
+        // Since we might kill the WebView, remove it from the
+        // content view first.
+        mUi.detachTab(appTab);
+        // Recreate the main WebView after destroying the old one.
+        mTabControl.recreateWebView(appTab);
+        // TODO: analyze why the remove and add are necessary
+        mUi.attachTab(appTab);
+        if (mTabControl.getCurrentTab() != appTab) {
+            switchToTab(appTab);
+            loadUrlDataIn(appTab, urlData);
+        } else {
+            // If the tab was the current tab, we have to attach
+            // it to the view system again.
+            setActiveTab(appTab);
+            loadUrlDataIn(appTab, urlData);
+        }
+    }
+
+    // Remove the sub window if it exists. Also called by TabControl when the
+    // user clicks the 'X' to dismiss a sub window.
+    @Override
+    public void dismissSubWindow(Tab tab) {
+        removeSubWindow(tab);
+        // dismiss the subwindow. This will destroy the WebView.
+        tab.dismissSubWindow();
+        WebView wv = getCurrentTopWebView();
+        if (wv != null) {
+            wv.requestFocus();
+        }
+    }
+
+    @Override
+    public void removeSubWindow(Tab t) {
+        if (t.getSubWebView() != null) {
+            mUi.removeSubWindow(t.getSubViewContainer());
+        }
+    }
+
+    @Override
+    public void attachSubWindow(Tab tab) {
+        if (tab.getSubWebView() != null) {
+            mUi.attachSubWindow(tab.getSubViewContainer());
+            getCurrentTopWebView().requestFocus();
+        }
+    }
+
+    private Tab showPreloadedTab(final UrlData urlData) {
+        if (!urlData.isPreloaded()) {
+            return null;
+        }
+        final PreloadedTabControl tabControl = urlData.getPreloadedTab();
+        final String sbQuery = urlData.getSearchBoxQueryToSubmit();
+        if (sbQuery != null) {
+            if (!tabControl.searchBoxSubmit(sbQuery, urlData.mUrl, urlData.mHeaders)) {
+                // Could not submit query. Fallback to regular tab creation
+                tabControl.destroy();
+                return null;
+            }
+        }
+        // check tab count and make room for new tab
+        if (!mTabControl.canCreateNewTab()) {
+            Tab leastUsed = mTabControl.getLeastUsedTab(getCurrentTab());
+            if (leastUsed != null) {
+                closeTab(leastUsed);
+            }
+        }
+        Tab t = tabControl.getTab();
+        t.refreshIdAfterPreload();
+        mTabControl.addPreloadedTab(t);
+        addTab(t);
+        setActiveTab(t);
+        return t;
+    }
+
+    // open a non inconito tab with the given url data
+    // and set as active tab
+    public Tab openTab(UrlData urlData) {
+        Tab tab = showPreloadedTab(urlData);
+        if (tab == null) {
+            tab = createNewTab(false, true, true);
+            if ((tab != null) && !urlData.isEmpty()) {
+                loadUrlDataIn(tab, urlData);
+            }
+        }
+        return tab;
+    }
+
+    @Override
+    public Tab openTabToHomePage() {
+        return openTab(mSettings.getHomePage(), false, true, false);
+    }
+
+    @Override
+    public Tab openIncognitoTab() {
+        return openTab(INCOGNITO_URI, true, true, false);
+    }
+
+    @Override
+    public Tab openTab(String url, boolean incognito, boolean setActive,
+            boolean useCurrent) {
+        return openTab(url, incognito, setActive, useCurrent, null);
+    }
+
+    @Override
+    public Tab openTab(String url, Tab parent, boolean setActive,
+            boolean useCurrent) {
+        return openTab(url, (parent != null) && parent.isPrivateBrowsingEnabled(),
+                setActive, useCurrent, parent);
+    }
+
+    public Tab openTab(String url, boolean incognito, boolean setActive,
+            boolean useCurrent, Tab parent) {
+        Tab tab = createNewTab(incognito, setActive, useCurrent);
+        if (tab != null) {
+            if (parent != null && parent != tab) {
+                parent.addChildTab(tab);
+            }
+            if (url != null) {
+                loadUrl(tab, url);
+            }
+        }
+        return tab;
+    }
+
+    // this method will attempt to create a new tab
+    // incognito: private browsing tab
+    // setActive: ste tab as current tab
+    // useCurrent: if no new tab can be created, return current tab
+    private Tab createNewTab(boolean incognito, boolean setActive,
+            boolean useCurrent) {
+        Tab tab = null;
+        MemoryMonitor memMonitor = null;
+        if (mTabControl.canCreateNewTab()) {
+            if (mSettings.enableMemoryMonitor()) {
+                Log.d(LOGTAG, " Memory Monitor Enabled .");
+                memMonitor = MemoryMonitor.getInstance(mActivity.getApplicationContext(),this);
+                if (memMonitor != null) {
+                    //Remove webview associated with the oldest tab
+                    memMonitor.destroyLeastRecentlyActiveTab();
+                }
+            } else {
+                Log.d(LOGTAG, " Memory Monitor disabled .");
+            }
+            tab = mTabControl.createNewTab(incognito);
+            addTab(tab);
+            tab.setTimeStamp();
+            if (setActive) {
+                setActiveTab(tab);
+            }
+        } else {
+            if (useCurrent) {
+                tab = mTabControl.getCurrentTab();
+                reuseTab(tab, null);
+            } else {
+                mUi.showMaxTabsWarning();
+            }
+        }
+        return tab;
+    }
+
+    @Override
+    public SnapshotTab createNewSnapshotTab(long snapshotId, boolean setActive) {
+        SnapshotTab tab = null;
+        if (mTabControl.canCreateNewTab()) {
+            tab = mTabControl.createSnapshotTab(snapshotId);
+            addTab(tab);
+            if (setActive) {
+                setActiveTab(tab);
+            }
+        } else {
+            mUi.showMaxTabsWarning();
+        }
+        return tab;
+    }
+
+    /**
+     * @param tab the tab to switch to
+     * @return boolean True if we successfully switched to a different tab.  If
+     *                 the indexth tab is null, or if that tab is the same as
+     *                 the current one, return false.
+     */
+    @Override
+    public boolean switchToTab(Tab tab) {
+        Tab currentTab = mTabControl.getCurrentTab();
+        if (tab == null || tab == currentTab) {
+            return false;
+        }
+        setActiveTab(tab);
+        return true;
+    }
+
+    @Override
+    public void closeCurrentTab() {
+        closeCurrentTab(false);
+    }
+
+    protected void closeCurrentTab(boolean andQuit) {
+        if (mTabControl.getTabCount() == 1) {
+            mCrashRecoveryHandler.clearState();
+            mTabControl.removeTab(getCurrentTab());
+            mActivity.finish();
+            return;
+        }
+        final Tab current = mTabControl.getCurrentTab();
+        final int pos = mTabControl.getCurrentPosition();
+        Tab newTab = current.getParent();
+        if (newTab == null) {
+            newTab = mTabControl.getTab(pos + 1);
+            if (newTab == null) {
+                newTab = mTabControl.getTab(pos - 1);
+            }
+        }
+        if (andQuit) {
+            mTabControl.setCurrentTab(newTab);
+            closeTab(current);
+        } else if (switchToTab(newTab)) {
+            // Close window
+            closeTab(current);
+        }
+    }
+
+    /**
+     * Close the tab, remove its associated title bar, and adjust mTabControl's
+     * current tab to a valid value.
+     */
+    @Override
+    public void closeTab(Tab tab) {
+        if (tab == mTabControl.getCurrentTab()) {
+            closeCurrentTab();
+        } else {
+            removeTab(tab);
+        }
+    }
+
+    /**
+     * Close all tabs except the current one
+     */
+    @Override
+    public void closeOtherTabs() {
+        int inactiveTabs = mTabControl.getTabCount() - 1;
+        for (int i = inactiveTabs; i >= 0; i--) {
+            Tab tab = mTabControl.getTab(i);
+            if (tab != mTabControl.getCurrentTab()) {
+                removeTab(tab);
+            }
+        }
+    }
+
+    // Called when loading from context menu or LOAD_URL message
+    protected void loadUrlFromContext(String url) {
+        Tab tab = getCurrentTab();
+        WebView view = tab != null ? tab.getWebView() : null;
+        // In case the user enters nothing.
+        if (url != null && url.length() != 0 && tab != null && view != null) {
+            url = UrlUtils.smartUrlFilter(url);
+            if (!((BrowserWebView) view).getWebViewClient().
+                    shouldOverrideUrlLoading(view, url)) {
+                loadUrl(tab, url);
+            }
+        }
+    }
+
+    /**
+     * Load the URL into the given WebView and update the title bar
+     * to reflect the new load.  Call this instead of WebView.loadUrl
+     * directly.
+     * @param view The WebView used to load url.
+     * @param url The URL to load.
+     */
+    @Override
+    public void loadUrl(Tab tab, String url) {
+        loadUrl(tab, url, null);
+    }
+
+    protected void loadUrl(Tab tab, String url, Map<String, String> headers) {
+        if (tab != null) {
+            dismissSubWindow(tab);
+            tab.loadUrl(url, headers);
+            if (tab.hasCrashed) {
+                tab.replaceCrashView(tab.getWebView(), tab.getViewContainer());
+            }
+            mUi.onProgressChanged(tab);
+        }
+    }
+
+    /**
+     * Load UrlData into a Tab and update the title bar to reflect the new
+     * load.  Call this instead of UrlData.loadIn directly.
+     * @param t The Tab used to load.
+     * @param data The UrlData being loaded.
+     */
+    protected void loadUrlDataIn(Tab t, UrlData data) {
+        if (data != null) {
+            if (data.isPreloaded()) {
+                // this isn't called for preloaded tabs
+            } else {
+                if (t != null && data.mDisableUrlOverride) {
+                    t.disableUrlOverridingForLoad();
+                }
+                loadUrl(t, data.mUrl, data.mHeaders);
+            }
+        }
+    }
+
+    @Override
+    public void onUserCanceledSsl(Tab tab) {
+        // TODO: Figure out the "right" behavior
+        if (tab.canGoBack()) {
+            tab.goBack();
+        } else {
+            tab.loadUrl(mSettings.getHomePage(), null);
+        }
+    }
+
+    void goBackOnePageOrQuit() {
+        Tab current = mTabControl.getCurrentTab();
+        if (current == null) {
+            /*
+             * Instead of finishing the activity, simply push this to the back
+             * of the stack and let ActivityManager to choose the foreground
+             * activity. As BrowserActivity is singleTask, it will be always the
+             * root of the task. So we can use either true or false for
+             * moveTaskToBack().
+             */
+            showExitDialog(mActivity);
+            return;
+        }
+        if (current.canGoBack()) {
+            current.goBack();
+        } else {
+            // Check to see if we are closing a window that was created by
+            // another window. If so, we switch back to that window.
+            Tab parent = current.getParent();
+            if (parent != null) {
+                switchToTab(parent);
+                // Now we close the other tab
+                closeTab(current);
+            } else {
+                /*
+                 * Instead of finishing the activity, simply push this to the back
+                 * of the stack and let ActivityManager to choose the foreground
+                 * activity. As BrowserActivity is singleTask, it will be always the
+                 * root of the task. So we can use either true or false for
+                 * moveTaskToBack().
+                 */
+                showExitDialog(mActivity);
+            }
+        }
+    }
+
+    /**
+     * helper method for key handler
+     * returns the current tab if it can't advance
+     */
+    private Tab getNextTab() {
+        int pos = mTabControl.getCurrentPosition() + 1;
+        if (pos >= mTabControl.getTabCount()) {
+            pos = 0;
+        }
+        return mTabControl.getTab(pos);
+    }
+
+    /**
+     * helper method for key handler
+     * returns the current tab if it can't advance
+     */
+    private Tab getPrevTab() {
+        int pos  = mTabControl.getCurrentPosition() - 1;
+        if ( pos < 0) {
+            pos = mTabControl.getTabCount() - 1;
+        }
+        return  mTabControl.getTab(pos);
+    }
+
+    boolean isMenuOrCtrlKey(int keyCode) {
+        return (KeyEvent.KEYCODE_MENU == keyCode)
+                || (KeyEvent.KEYCODE_CTRL_LEFT == keyCode)
+                || (KeyEvent.KEYCODE_CTRL_RIGHT == keyCode);
+    }
+
+    /**
+     * handle key events in browser
+     *
+     * @param keyCode
+     * @param event
+     * @return true if handled, false to pass to super
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        boolean noModifiers = event.hasNoModifiers();
+        // Even if MENU is already held down, we need to call to super to open
+        // the IME on long press.
+        if (!noModifiers && isMenuOrCtrlKey(keyCode)) {
+            mMenuIsDown = true;
+            return false;
+        }
+
+        WebView webView = getCurrentTopWebView();
+        Tab tab = getCurrentTab();
+        if (webView == null || tab == null) return false;
+
+        boolean ctrl = event.hasModifiers(KeyEvent.META_CTRL_ON);
+        boolean shift = event.hasModifiers(KeyEvent.META_SHIFT_ON);
+
+        switch(keyCode) {
+            case KeyEvent.KEYCODE_TAB:
+                if (event.isCtrlPressed()) {
+                    if (event.isShiftPressed()) {
+                        // prev tab
+                        switchToTab(getPrevTab());
+                    } else {
+                        // next tab
+                        switchToTab(getNextTab());
+                    }
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_SPACE:
+                // WebView/WebTextView handle the keys in the KeyDown. As
+                // the Activity's shortcut keys are only handled when WebView
+                // doesn't, have to do it in onKeyDown instead of onKeyUp.
+                if (shift) {
+                    pageUp();
+                } else if (noModifiers) {
+                    pageDown();
+                }
+                return true;
+            case KeyEvent.KEYCODE_BACK:
+                if (!noModifiers) break;
+                event.startTracking();
+                return true;
+            case KeyEvent.KEYCODE_FORWARD:
+                if (!noModifiers) break;
+                tab.goForward();
+                return true;
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                if (ctrl) {
+                    tab.goBack();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (ctrl) {
+                    tab.goForward();
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_A:
+                if (ctrl) {
+                    webView.selectAll();
+                    return true;
+                }
+                break;
+//          case KeyEvent.KEYCODE_B:    // menu
+            case KeyEvent.KEYCODE_C:
+                if (ctrl ) {
+                    webView.copySelection();
+                    return true;
+                }
+                break;
+//          case KeyEvent.KEYCODE_D:    // menu
+//          case KeyEvent.KEYCODE_E:    // in Chrome: puts '?' in URL bar
+//          case KeyEvent.KEYCODE_F:    // menu
+//          case KeyEvent.KEYCODE_G:    // in Chrome: finds next match
+//          case KeyEvent.KEYCODE_H:    // menu
+//          case KeyEvent.KEYCODE_I:    // unused
+//          case KeyEvent.KEYCODE_J:    // menu
+//          case KeyEvent.KEYCODE_K:    // in Chrome: puts '?' in URL bar
+//          case KeyEvent.KEYCODE_L:    // menu
+//          case KeyEvent.KEYCODE_M:    // unused
+//          case KeyEvent.KEYCODE_N:    // in Chrome: new window
+//          case KeyEvent.KEYCODE_O:    // in Chrome: open file
+//          case KeyEvent.KEYCODE_P:    // in Chrome: print page
+//          case KeyEvent.KEYCODE_Q:    // unused
+//          case KeyEvent.KEYCODE_R:
+//          case KeyEvent.KEYCODE_S:    // in Chrome: saves page
+            case KeyEvent.KEYCODE_T:
+                // we can't use the ctrl/shift flags, they check for
+                // exclusive use of a modifier
+                if (event.isCtrlPressed()) {
+                    if (event.isShiftPressed()) {
+                        openIncognitoTab();
+                    } else {
+                        openTabToHomePage();
+                    }
+                    return true;
+                }
+                break;
+//          case KeyEvent.KEYCODE_U:    // in Chrome: opens source of page
+//          case KeyEvent.KEYCODE_V:    // text view intercepts to paste
+//          case KeyEvent.KEYCODE_W:    // menu
+//          case KeyEvent.KEYCODE_X:    // text view intercepts to cut
+//          case KeyEvent.KEYCODE_Y:    // unused
+//          case KeyEvent.KEYCODE_Z:    // unused
+        }
+        // it is a regular key and webview is not null
+         return mUi.dispatchKey(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+        switch(keyCode) {
+        case KeyEvent.KEYCODE_BACK:
+            if (mUi.isWebShowing()) {
+                bookmarksOrHistoryPicker(ComboViews.History);
+                return true;
+            }
+            break;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (isMenuOrCtrlKey(keyCode)) {
+            mMenuIsDown = false;
+            if (KeyEvent.KEYCODE_MENU == keyCode
+                    && event.isTracking() && !event.isCanceled()) {
+                return onMenuKey();
+            }
+        }
+        if (!event.hasNoModifiers()) return false;
+        switch(keyCode) {
+            case KeyEvent.KEYCODE_BACK:
+                if (event.isTracking() && !event.isCanceled()) {
+                    onBackKey();
+                    return true;
+                }
+                break;
+        }
+        return false;
+    }
+
+    public boolean isMenuDown() {
+        return mMenuIsDown;
+    }
+
+    @Override
+    public void setupAutoFill(Message message) {
+        // Open the settings activity at the AutoFill profile fragment so that
+        // the user can create a new profile. When they return, we will dispatch
+        // the message so that we can autofill the form using their new profile.
+        Intent intent = new Intent(mActivity, BrowserPreferencesPage.class);
+        intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
+                AutoFillSettingsFragment.class.getName());
+        mAutoFillSetupMessage = message;
+        mActivity.startActivityForResult(intent, AUTOFILL_SETUP);
+    }
+
+    @Override
+    public boolean onSearchRequested() {
+        mUi.editUrl(false, true);
+        return true;
+    }
+
+    @Override
+    public boolean shouldCaptureThumbnails() {
+        return mUi.shouldCaptureThumbnails();
+    }
+
+    @Override
+    public boolean supportsVoice() {
+        PackageManager pm = mActivity.getPackageManager();
+        List activities = pm.queryIntentActivities(new Intent(
+                RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0);
+        return activities.size() != 0;
+    }
+
+    @Override
+    public void startVoiceRecognizer() {
+        Intent voice = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+        voice.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+                RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
+        voice.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 1);
+        mActivity.startActivityForResult(voice, VOICE_RESULT);
+    }
+
+    @Override
+    public void setBlockEvents(boolean block) {
+        mBlockEvents = block;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return mBlockEvents;
+    }
+
+    @Override
+    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+        return mBlockEvents;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return mBlockEvents;
+    }
+
+    @Override
+    public boolean dispatchTrackballEvent(MotionEvent ev) {
+        return mBlockEvents;
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+        return mBlockEvents;
+    }
+
+}
diff --git a/src/com/android/browser/CrashRecoveryHandler.java b/src/com/android/browser/CrashRecoveryHandler.java
new file mode 100644
index 0000000..bcdf8b0
--- /dev/null
+++ b/src/com/android/browser/CrashRecoveryHandler.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcel;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class CrashRecoveryHandler {
+
+    private static final boolean LOGV_ENABLED = Browser.LOGV_ENABLED;
+    private static final String LOGTAG = "BrowserCrashRecovery";
+    private static final String STATE_FILE = "browser_state.parcel";
+    private static final int BUFFER_SIZE = 4096;
+    private static final long BACKUP_DELAY = 500; // 500ms between writes
+    /* This is the duration for which we will prompt to restore
+     * instead of automatically restoring. The first time the browser crashes,
+     * we will automatically restore. If we then crash again within XX minutes,
+     * we will prompt instead of automatically restoring.
+     */
+    private static final long PROMPT_INTERVAL = 5 * 60 * 1000; // 5 minutes
+
+    private static final int MSG_WRITE_STATE = 1;
+    private static final int MSG_CLEAR_STATE = 2;
+    private static final int MSG_PRELOAD_STATE = 3;
+
+    private static CrashRecoveryHandler sInstance;
+
+    private Controller mController;
+    private Context mContext;
+    private Handler mForegroundHandler;
+    private Handler mBackgroundHandler;
+    private boolean mIsPreloading = false;
+    private boolean mDidPreload = false;
+    private Bundle mRecoveryState = null;
+
+    public static CrashRecoveryHandler initialize(Controller controller) {
+        if (sInstance == null) {
+            sInstance = new CrashRecoveryHandler(controller);
+        } else {
+            sInstance.mController = controller;
+        }
+        return sInstance;
+    }
+
+    public static CrashRecoveryHandler getInstance() {
+        return sInstance;
+    }
+
+    private CrashRecoveryHandler(Controller controller) {
+        mController = controller;
+        mContext = mController.getActivity().getApplicationContext();
+        mForegroundHandler = new Handler();
+        mBackgroundHandler = new Handler(BackgroundHandler.getLooper()) {
+
+            @Override
+            public void handleMessage(Message msg) {
+                switch (msg.what) {
+                case MSG_WRITE_STATE:
+                    Bundle saveState = (Bundle) msg.obj;
+                    writeState(saveState);
+                    break;
+                case MSG_CLEAR_STATE:
+                    if (LOGV_ENABLED) {
+                        Log.v(LOGTAG, "Clearing crash recovery state");
+                    }
+                    File state = new File(mContext.getCacheDir(), STATE_FILE);
+                    if (state.exists()) {
+                        state.delete();
+                    }
+                    break;
+                case MSG_PRELOAD_STATE:
+                    mRecoveryState = loadCrashState();
+                    synchronized (CrashRecoveryHandler.this) {
+                        mIsPreloading = false;
+                        mDidPreload = true;
+                        CrashRecoveryHandler.this.notifyAll();
+                    }
+                    break;
+                }
+            }
+        };
+    }
+
+    public void backupState() {
+        mForegroundHandler.postDelayed(mCreateState, BACKUP_DELAY);
+    }
+
+    private Runnable mCreateState = new Runnable() {
+
+        @Override
+        public void run() {
+            try {
+                final Bundle state = mController.createSaveState();
+                Message.obtain(mBackgroundHandler, MSG_WRITE_STATE, state)
+                        .sendToTarget();
+                // Remove any queued up saves
+                mForegroundHandler.removeCallbacks(mCreateState);
+            } catch (Throwable t) {
+                Log.w(LOGTAG, "Failed to save state", t);
+                return;
+            }
+        }
+
+    };
+
+    public void clearState() {
+        clearState(false);
+    }
+
+    /**
+     * Clear cached state files.
+     *
+     * @param block If block, clear state files in the caller thread, otherwise
+     * do it in a worker thread.
+     */
+    void clearState(boolean block) {
+        if (block) {
+            if (mContext != null) {
+                File state = new File(mContext.getCacheDir(), STATE_FILE);
+                if (state.exists()) {
+                    state.delete();
+                }
+            }
+        } else {
+            mBackgroundHandler.sendEmptyMessage(MSG_CLEAR_STATE);
+        }
+        updateLastRecovered(0);
+    }
+
+    private boolean shouldRestore() {
+        BrowserSettings browserSettings = BrowserSettings.getInstance();
+        long lastRecovered = browserSettings.getLastRecovered();
+        long timeSinceLastRecover = System.currentTimeMillis() - lastRecovered;
+        return (timeSinceLastRecover > PROMPT_INTERVAL)
+                || browserSettings.wasLastRunPaused();
+    }
+
+    private void updateLastRecovered(long time) {
+        BrowserSettings browserSettings = BrowserSettings.getInstance();
+        browserSettings.setLastRecovered(time);
+    }
+
+    synchronized private Bundle loadCrashState() {
+        if (!shouldRestore()) {
+            return null;
+        }
+        BrowserSettings browserSettings = BrowserSettings.getInstance();
+        browserSettings.setLastRunPaused(false);
+        Bundle state = null;
+        Parcel parcel = Parcel.obtain();
+        FileInputStream fin = null;
+        try {
+            File stateFile = new File(mContext.getCacheDir(), STATE_FILE);
+            fin = new FileInputStream(stateFile);
+            ByteArrayOutputStream dataStream = new ByteArrayOutputStream();
+            byte[] buffer = new byte[BUFFER_SIZE];
+            int read;
+            while ((read = fin.read(buffer)) > 0) {
+                dataStream.write(buffer, 0, read);
+            }
+            byte[] data = dataStream.toByteArray();
+            parcel.unmarshall(data, 0, data.length);
+            parcel.setDataPosition(0);
+            state = parcel.readBundle();
+            if (state != null && !state.isEmpty()) {
+                return state;
+            }
+        } catch (FileNotFoundException e) {
+            // No state to recover
+        } catch (Throwable e) {
+            Log.w(LOGTAG, "Failed to recover state!", e);
+        } finally {
+            parcel.recycle();
+            if (fin != null) {
+                try {
+                    fin.close();
+                } catch (IOException e) { }
+            }
+        }
+        return null;
+    }
+
+    public void startRecovery(Intent intent) {
+        synchronized (CrashRecoveryHandler.this) {
+            while (mIsPreloading) {
+                try {
+                    CrashRecoveryHandler.this.wait();
+                } catch (InterruptedException e) {}
+            }
+        }
+        if (!mDidPreload) {
+            mRecoveryState = loadCrashState();
+        }
+        updateLastRecovered(mRecoveryState != null
+                ? System.currentTimeMillis() : 0);
+        mController.doStart(mRecoveryState, intent);
+        mRecoveryState = null;
+    }
+
+    public void preloadCrashState() {
+        synchronized (CrashRecoveryHandler.this) {
+            if (mIsPreloading) {
+                return;
+            }
+            mIsPreloading = true;
+        }
+        mBackgroundHandler.sendEmptyMessage(MSG_PRELOAD_STATE);
+    }
+
+    /**
+     * Writes the crash recovery state to a file synchronously.
+     * Errors are swallowed, but logged.
+     * @param state The state to write out
+     */
+    synchronized void writeState(Bundle state) {
+        if (LOGV_ENABLED) {
+            Log.v(LOGTAG, "Saving crash recovery state");
+        }
+        Parcel p = Parcel.obtain();
+        try {
+            state.writeToParcel(p, 0);
+            File stateJournal = new File(mContext.getCacheDir(),
+                    STATE_FILE + ".journal");
+            FileOutputStream fout = new FileOutputStream(stateJournal);
+            fout.write(p.marshall());
+            fout.close();
+            File stateFile = new File(mContext.getCacheDir(),
+                    STATE_FILE);
+            if (!stateJournal.renameTo(stateFile)) {
+                // Failed to rename, try deleting the existing
+                // file and try again
+                stateFile.delete();
+                stateJournal.renameTo(stateFile);
+            }
+        } catch (Throwable e) {
+            Log.i(LOGTAG, "Failed to save persistent state", e);
+        } finally {
+            p.recycle();
+        }
+    }
+}
diff --git a/src/com/android/browser/DataController.java b/src/com/android/browser/DataController.java
new file mode 100644
index 0000000..eb47080
--- /dev/null
+++ b/src/com/android/browser/DataController.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2010 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.History;
+import com.android.browser.provider.BrowserProvider2.Thumbnails;
+
+import java.nio.ByteBuffer;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+public class DataController {
+    private static final String LOGTAG = "DataController";
+    // Message IDs
+    private static final int HISTORY_UPDATE_VISITED = 100;
+    private static final int HISTORY_UPDATE_TITLE = 101;
+    private static final int QUERY_URL_IS_BOOKMARK = 200;
+    private static final int TAB_LOAD_THUMBNAIL = 201;
+    private static final int TAB_SAVE_THUMBNAIL = 202;
+    private static final int TAB_DELETE_THUMBNAIL = 203;
+    private static DataController sInstance;
+
+    private Context mContext;
+    private DataControllerHandler mDataHandler;
+    private Handler mCbHandler; // To respond on the UI thread
+    private ByteBuffer mBuffer; // to capture thumbnails
+
+    /* package */ static interface OnQueryUrlIsBookmark {
+        void onQueryUrlIsBookmark(String url, boolean isBookmark);
+    }
+    private static class CallbackContainer {
+        Object replyTo;
+        Object[] args;
+    }
+
+    private static class DCMessage {
+        int what;
+        Object obj;
+        Object replyTo;
+        DCMessage(int w, Object o) {
+            what = w;
+            obj = o;
+        }
+    }
+
+    /* package */ static DataController getInstance(Context c) {
+        if (sInstance == null) {
+            sInstance = new DataController(c);
+        }
+        return sInstance;
+    }
+
+    private DataController(Context c) {
+        mContext = c.getApplicationContext();
+        mDataHandler = new DataControllerHandler();
+        mDataHandler.start();
+        mCbHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                CallbackContainer cc = (CallbackContainer) msg.obj;
+                switch (msg.what) {
+                    case QUERY_URL_IS_BOOKMARK: {
+                        OnQueryUrlIsBookmark cb = (OnQueryUrlIsBookmark) cc.replyTo;
+                        String url = (String) cc.args[0];
+                        boolean isBookmark = (Boolean) cc.args[1];
+                        cb.onQueryUrlIsBookmark(url, isBookmark);
+                        break;
+                    }
+                }
+            }
+        };
+    }
+
+    public void updateVisitedHistory(String url) {
+        mDataHandler.sendMessage(HISTORY_UPDATE_VISITED, url);
+    }
+
+    public void updateHistoryTitle(String url, String title) {
+        mDataHandler.sendMessage(HISTORY_UPDATE_TITLE, new String[] { url, title });
+    }
+
+    public void queryBookmarkStatus(String url, OnQueryUrlIsBookmark replyTo) {
+        if (url == null || url.trim().length() == 0) {
+            // null or empty url is never a bookmark
+            replyTo.onQueryUrlIsBookmark(url, false);
+            return;
+        }
+        mDataHandler.sendMessage(QUERY_URL_IS_BOOKMARK, url.trim(), replyTo);
+    }
+
+    public void loadThumbnail(Tab tab) {
+        mDataHandler.sendMessage(TAB_LOAD_THUMBNAIL, tab);
+    }
+
+    public void deleteThumbnail(Tab tab) {
+        mDataHandler.sendMessage(TAB_DELETE_THUMBNAIL, tab.getId());
+    }
+
+    public void saveThumbnail(Tab tab) {
+        mDataHandler.sendMessage(TAB_SAVE_THUMBNAIL, tab);
+    }
+
+    // The standard Handler and Message classes don't allow the queue manipulation
+    // we want (such as peeking). So we use our own queue.
+    class DataControllerHandler extends Thread {
+        private BlockingQueue<DCMessage> mMessageQueue
+                = new LinkedBlockingQueue<DCMessage>();
+
+        public DataControllerHandler() {
+            super("DataControllerHandler");
+        }
+
+        @Override
+        public void run() {
+            setPriority(Thread.MIN_PRIORITY);
+            while (true) {
+                try {
+                    handleMessage(mMessageQueue.take());
+                } catch (InterruptedException ex) {
+                    break;
+                }
+            }
+        }
+
+        void sendMessage(int what, Object obj) {
+            DCMessage m = new DCMessage(what, obj);
+            mMessageQueue.add(m);
+        }
+
+        void sendMessage(int what, Object obj, Object replyTo) {
+            DCMessage m = new DCMessage(what, obj);
+            m.replyTo = replyTo;
+            mMessageQueue.add(m);
+        }
+
+        private void handleMessage(DCMessage msg) {
+            switch (msg.what) {
+            case HISTORY_UPDATE_VISITED:
+                doUpdateVisitedHistory((String) msg.obj);
+                break;
+            case HISTORY_UPDATE_TITLE:
+                String[] args = (String[]) msg.obj;
+                doUpdateHistoryTitle(args[0], args[1]);
+                break;
+            case QUERY_URL_IS_BOOKMARK:
+                // TODO: Look for identical messages in the queue and remove them
+                // TODO: Also, look for partial matches and merge them (such as
+                //       multiple callbacks querying the same URL)
+                doQueryBookmarkStatus((String) msg.obj, msg.replyTo);
+                break;
+            case TAB_LOAD_THUMBNAIL:
+                doLoadThumbnail((Tab) msg.obj);
+                break;
+            case TAB_DELETE_THUMBNAIL:
+                ContentResolver cr = mContext.getContentResolver();
+                try {
+                    cr.delete(ContentUris.withAppendedId(
+                            Thumbnails.CONTENT_URI, (Long)msg.obj),
+                            null, null);
+                } catch (Throwable t) {}
+                break;
+            case TAB_SAVE_THUMBNAIL:
+                doSaveThumbnail((Tab)msg.obj);
+                break;
+            }
+        }
+
+        private byte[] getCaptureBlob(Tab tab) {
+            synchronized (tab) {
+                Bitmap capture = tab.getScreenshot();
+                if (capture == null) {
+                    return null;
+                }
+                if (mBuffer == null || mBuffer.limit() < capture.getByteCount()) {
+                    mBuffer = ByteBuffer.allocate(capture.getByteCount());
+                }
+                capture.copyPixelsToBuffer(mBuffer);
+                mBuffer.rewind();
+                return mBuffer.array();
+            }
+        }
+
+        private void doSaveThumbnail(Tab tab) {
+            byte[] blob = getCaptureBlob(tab);
+            if (blob == null) {
+                return;
+            }
+            ContentResolver cr = mContext.getContentResolver();
+            ContentValues values = new ContentValues();
+            values.put(Thumbnails._ID, tab.getId());
+            values.put(Thumbnails.THUMBNAIL, blob);
+            cr.insert(Thumbnails.CONTENT_URI, values);
+        }
+
+        private void doLoadThumbnail(Tab tab) {
+            ContentResolver cr = mContext.getContentResolver();
+            Cursor c = null;
+            try {
+                Uri uri = ContentUris.withAppendedId(Thumbnails.CONTENT_URI, tab.getId());
+                c = cr.query(uri, new String[] {Thumbnails._ID,
+                        Thumbnails.THUMBNAIL}, null, null, null);
+                if (c.moveToFirst()) {
+                    byte[] data = c.getBlob(1);
+                    if (data != null && data.length > 0) {
+                        tab.updateCaptureFromBlob(data);
+                    }
+                }
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+        }
+
+        private void doUpdateVisitedHistory(String url) {
+            ContentResolver cr = mContext.getContentResolver();
+            Cursor c = null;
+            try {
+                c = cr.query(History.CONTENT_URI, new String[] {History._ID, History.VISITS},
+                        History.URL + "=?", new String[] { url }, null);
+                if (c.moveToFirst()) {
+                    ContentValues values = new ContentValues();
+                    values.put(History.VISITS, c.getInt(1) + 1);
+                    values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
+                    cr.update(ContentUris.withAppendedId(History.CONTENT_URI, c.getLong(0)),
+                            values, null, null);
+                } else {
+                    android.provider.Browser.truncateHistory(cr);
+                    ContentValues values = new ContentValues();
+                    values.put(History.URL, url);
+                    values.put(History.VISITS, 1);
+                    values.put(History.DATE_LAST_VISITED, System.currentTimeMillis());
+                    values.put(History.TITLE, url);
+                    values.put(History.DATE_CREATED, 0);
+                    values.put(History.USER_ENTERED, 0);
+                    cr.insert(History.CONTENT_URI, values);
+                }
+            } finally {
+                if (c != null) c.close();
+            }
+        }
+
+        private void doQueryBookmarkStatus(String url, Object replyTo) {
+            // Check to see if the site is bookmarked
+            Cursor cursor = null;
+            boolean isBookmark = false;
+            try {
+                cursor = mContext.getContentResolver().query(
+                        BookmarkUtils.getBookmarksUri(mContext),
+                        new String[] { BrowserContract.Bookmarks.URL },
+                        BrowserContract.Bookmarks.URL + " == ?",
+                        new String[] { url },
+                        null);
+                isBookmark = cursor.moveToFirst();
+            } catch (SQLiteException e) {
+                Log.e(LOGTAG, "Error checking for bookmark: " + e);
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+            CallbackContainer cc = new CallbackContainer();
+            cc.replyTo = replyTo;
+            cc.args = new Object[] { url, isBookmark };
+            mCbHandler.obtainMessage(QUERY_URL_IS_BOOKMARK, cc).sendToTarget();
+        }
+
+        private void doUpdateHistoryTitle(String url, String title) {
+            ContentResolver cr = mContext.getContentResolver();
+            ContentValues values = new ContentValues();
+            values.put(History.TITLE, title);
+            cr.update(History.CONTENT_URI, values, History.URL + "=?",
+                    new String[] { url });
+        }
+    }
+}
diff --git a/src/com/android/browser/DataUri.java b/src/com/android/browser/DataUri.java
new file mode 100644
index 0000000..dae3caf
--- /dev/null
+++ b/src/com/android/browser/DataUri.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 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 java.net.MalformedURLException;
+
+import android.util.Base64;
+/**
+ * Class extracts the mime type and data from a data uri.
+ * A data URI is of the form:
+ * <pre>
+ * data:[&lt;MIME-type&gt;][;charset=&lt;encoding&gt;][;base64],&lt;data&gt;
+ * </pre>
+ */
+public class DataUri {
+    private static final String DATA_URI_PREFIX = "data:";
+    private static final String BASE_64_ENCODING = ";base64";
+
+    private String mMimeType;
+    private byte[] mData;
+
+    public DataUri(String uri) throws MalformedURLException {
+        if (!isDataUri(uri)) {
+            throw new MalformedURLException("Not a data URI");
+        }
+
+        int commaIndex = uri.indexOf(',', DATA_URI_PREFIX.length());
+        if (commaIndex < 0) {
+            throw new MalformedURLException("Comma expected in data URI");
+        }
+        String contentType = uri.substring(DATA_URI_PREFIX.length(),
+                commaIndex);
+        mData = uri.substring(commaIndex + 1).getBytes();
+        if (contentType.contains(BASE_64_ENCODING)) {
+            mData = Base64.decode(mData, Base64.DEFAULT);
+        }
+        int semiIndex = contentType.indexOf(';');
+        if (semiIndex > 0) {
+            mMimeType = contentType.substring(0, semiIndex);
+        } else {
+            mMimeType = contentType;
+        }
+    }
+
+    /**
+     * Returns true if the text passed in appears to be a data URI.
+     */
+    public static boolean isDataUri(String text)
+    {
+        return text.startsWith(DATA_URI_PREFIX);
+    }
+
+    public String getMimeType() {
+        return mMimeType;
+    }
+
+    public byte[] getData() {
+        return mData;
+    }
+}
diff --git a/src/com/android/browser/DateSortedExpandableListAdapter.java b/src/com/android/browser/DateSortedExpandableListAdapter.java
new file mode 100644
index 0000000..529e1ed
--- /dev/null
+++ b/src/com/android/browser/DateSortedExpandableListAdapter.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright (C) 2010 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 com.android.browser.R;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.DateSorter;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.TextView;
+
+/**
+ * ExpandableListAdapter which separates data into categories based on date.
+ * Used for History and Downloads.
+ */
+public class DateSortedExpandableListAdapter extends BaseExpandableListAdapter {
+    // Array for each of our bins.  Each entry represents how many items are
+    // in that bin.
+    private int mItemMap[];
+    // This is our GroupCount.  We will have at most DateSorter.DAY_COUNT
+    // bins, less if the user has no items in one or more bins.
+    private int mNumberOfBins;
+    private Cursor mCursor;
+    private DateSorter mDateSorter;
+    private int mDateIndex;
+    private int mIdIndex;
+    private Context mContext;
+
+    boolean mDataValid;
+
+    DataSetObserver mDataSetObserver = new DataSetObserver() {
+        @Override
+        public void onChanged() {
+            mDataValid = true;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            mDataValid = false;
+            notifyDataSetInvalidated();
+        }
+    };
+    
+    public DateSortedExpandableListAdapter(Context context, int dateIndex) {
+        mContext = context;
+        mDateSorter = new DateSorter(context);
+        mDateIndex = dateIndex;
+        mDataValid = false;
+        mIdIndex = -1;
+    }
+
+    /**
+     * Set up the bins for determining which items belong to which groups.
+     */
+    private void buildMap() {
+        // The cursor is sorted by date
+        // The ItemMap will store the number of items in each bin.
+        int array[] = new int[DateSorter.DAY_COUNT];
+        // Zero out the array.
+        for (int j = 0; j < DateSorter.DAY_COUNT; j++) {
+            array[j] = 0;
+        }
+        mNumberOfBins = 0;
+        int dateIndex = -1;
+        if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
+            while (!mCursor.isAfterLast()) {
+                long date = getLong(mDateIndex);
+                int index = mDateSorter.getIndex(date);
+                if (index > dateIndex) {
+                    mNumberOfBins++;
+                    if (index == DateSorter.DAY_COUNT - 1) {
+                        // We are already in the last bin, so it will
+                        // include all the remaining items
+                        array[index] = mCursor.getCount()
+                                - mCursor.getPosition();
+                        break;
+                    }
+                    dateIndex = index;
+                }
+                array[dateIndex]++;
+                mCursor.moveToNext();
+            }
+        }
+        mItemMap = array;
+    }
+
+    /**
+     * Get the byte array at cursorIndex from the Cursor.  Assumes the Cursor
+     * has already been moved to the correct position.  Along with
+     * {@link #getInt} and {@link #getString}, these are provided so the client
+     * does not need to access the Cursor directly
+     * @param cursorIndex Index to query the Cursor.
+     * @return corresponding byte array from the Cursor.
+     */
+    /* package */ byte[] getBlob(int cursorIndex) {
+        if (!mDataValid) return null; 
+        return mCursor.getBlob(cursorIndex);
+    }
+
+    /* package */ Context getContext() {
+        return mContext;
+    }
+
+    /**
+     * Get the integer at cursorIndex from the Cursor.  Assumes the Cursor has
+     * already been moved to the correct position.  Along with
+     * {@link #getBlob} and {@link #getString}, these are provided so the client
+     * does not need to access the Cursor directly
+     * @param cursorIndex Index to query the Cursor.
+     * @return corresponding integer from the Cursor.
+     */
+    /* package */ int getInt(int cursorIndex) {
+        if (!mDataValid) return 0; 
+        return mCursor.getInt(cursorIndex);
+    }
+
+    /**
+     * Get the long at cursorIndex from the Cursor.  Assumes the Cursor has
+     * already been moved to the correct position.
+     */
+    /* package */ long getLong(int cursorIndex) {
+        if (!mDataValid) return 0; 
+        return mCursor.getLong(cursorIndex);
+    }
+
+    /**
+     * Get the String at cursorIndex from the Cursor.  Assumes the Cursor has
+     * already been moved to the correct position.  Along with
+     * {@link #getInt} and {@link #getInt}, these are provided so the client
+     * does not need to access the Cursor directly
+     * @param cursorIndex Index to query the Cursor.
+     * @return corresponding String from the Cursor.
+     */
+    /* package */ String getString(int cursorIndex) {
+        if (!mDataValid) return null; 
+        return mCursor.getString(cursorIndex);
+    }
+
+    /**
+     * Determine which group an item belongs to.
+     * @param childId ID of the child view in question.
+     * @return int Group position of the containing group.
+    /* package */ int groupFromChildId(long childId) {
+        if (!mDataValid) return -1; 
+        int group = -1;
+        for (mCursor.moveToFirst(); !mCursor.isAfterLast();
+                mCursor.moveToNext()) {
+            if (getLong(mIdIndex) == childId) {
+                int bin = mDateSorter.getIndex(getLong(mDateIndex));
+                // bin is the same as the group if the number of bins is the
+                // same as DateSorter
+                if (DateSorter.DAY_COUNT == mNumberOfBins) {
+                    return bin;
+                }
+                // There are some empty bins.  Find the corresponding group.
+                group = 0;
+                for (int i = 0; i < bin; i++) {
+                    if (mItemMap[i] != 0) {
+                        group++;
+                    }
+                }
+                break;
+            }
+        }
+        return group;
+    }
+
+    /**
+     * Translates from a group position in the ExpandableList to a bin.  This is
+     * necessary because some groups have no history items, so we do not include
+     * those in the ExpandableList.
+     * @param groupPosition Position in the ExpandableList's set of groups
+     * @return The corresponding bin that holds that group.
+     */
+    private int groupPositionToBin(int groupPosition) {
+        if (!mDataValid) return -1; 
+        if (groupPosition < 0 || groupPosition >= DateSorter.DAY_COUNT) {
+            throw new AssertionError("group position out of range");
+        }
+        if (DateSorter.DAY_COUNT == mNumberOfBins || 0 == mNumberOfBins) {
+            // In the first case, we have exactly the same number of bins
+            // as our maximum possible, so there is no need to do a
+            // conversion
+            // The second statement is in case this method gets called when
+            // the array is empty, in which case the provided groupPosition
+            // will do fine.
+            return groupPosition;
+        }
+        int arrayPosition = -1;
+        while (groupPosition > -1) {
+            arrayPosition++;
+            if (mItemMap[arrayPosition] != 0) {
+                groupPosition--;
+            }
+        }
+        return arrayPosition;
+    }
+
+    /**
+     * Move the cursor to the position indicated.
+     * @param packedPosition Position in packed position representation.
+     * @return True on success, false otherwise.
+     */
+    boolean moveCursorToPackedChildPosition(long packedPosition) {
+        if (ExpandableListView.getPackedPositionType(packedPosition) !=
+                ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+            return false;
+        }
+        int groupPosition = ExpandableListView.getPackedPositionGroup(
+                packedPosition);
+        int childPosition = ExpandableListView.getPackedPositionChild(
+                packedPosition);
+        return moveCursorToChildPosition(groupPosition, childPosition);
+    }
+
+    /**
+     * Move the cursor the the position indicated.
+     * @param groupPosition Index of the group containing the desired item.
+     * @param childPosition Index of the item within the specified group.
+     * @return boolean False if the cursor is closed, so the Cursor was not
+     *      moved.  True on success.
+     */
+    /* package */ boolean moveCursorToChildPosition(int groupPosition,
+            int childPosition) {
+        if (!mDataValid || mCursor.isClosed()) {
+            return false;
+        }
+        groupPosition = groupPositionToBin(groupPosition);
+        int index = childPosition;
+        for (int i = 0; i < groupPosition; i++) {
+            index += mItemMap[i];
+        }
+        return mCursor.moveToPosition(index);
+    }
+
+    public void changeCursor(Cursor cursor) {
+        if (cursor == mCursor) {
+            return;
+        }
+        if (mCursor != null) {
+            mCursor.unregisterDataSetObserver(mDataSetObserver);
+            mCursor.close();
+        }
+        mCursor = cursor;
+        if (cursor != null) {
+            cursor.registerDataSetObserver(mDataSetObserver);
+            mIdIndex = cursor.getColumnIndexOrThrow("_id");
+            mDataValid = true;
+            buildMap();
+            // notify the observers about the new cursor
+            notifyDataSetChanged();
+        } else {
+            mIdIndex = -1;
+            mDataValid = false;
+            // notify the observers about the lack of a data set
+            notifyDataSetInvalidated();
+        }
+    }
+
+    @Override
+    public View getGroupView(int groupPosition, boolean isExpanded,
+            View convertView, ViewGroup parent) {
+        if (!mDataValid) throw new IllegalStateException("Data is not valid"); 
+        TextView item;
+        if (null == convertView || !(convertView instanceof TextView)) {
+            LayoutInflater factory = LayoutInflater.from(mContext);
+            item = (TextView) factory.inflate(R.layout.history_header, null);
+        } else {
+            item = (TextView) convertView;
+        }
+        String label = mDateSorter.getLabel(groupPositionToBin(groupPosition));
+        item.setText(label);
+        return item;
+    }
+
+    @Override
+    public View getChildView(int groupPosition, int childPosition,
+            boolean isLastChild, View convertView, ViewGroup parent) {
+        if (!mDataValid) throw new IllegalStateException("Data is not valid"); 
+        return null;
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean isChildSelectable(int groupPosition, int childPosition) {
+        return true;
+    }
+
+    @Override
+    public int getGroupCount() {
+        if (!mDataValid) return 0;
+        return mNumberOfBins;
+    }
+
+    @Override
+    public int getChildrenCount(int groupPosition) {
+        if (!mDataValid) return 0;
+        return mItemMap[groupPositionToBin(groupPosition)];
+    }
+
+    @Override
+    public Object getGroup(int groupPosition) {
+        return null;
+    }
+
+    @Override
+    public Object getChild(int groupPosition, int childPosition) {
+        return null;
+    }
+
+    @Override
+    public long getGroupId(int groupPosition) {
+        if (!mDataValid) return 0; 
+        return groupPosition;
+    }
+
+    @Override
+    public long getChildId(int groupPosition, int childPosition) {
+        if (!mDataValid) return 0; 
+        if (moveCursorToChildPosition(groupPosition, childPosition)) {
+            return getLong(mIdIndex);
+        }
+        return 0;
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    @Override
+    public void onGroupExpanded(int groupPosition) {
+    }
+
+    @Override
+    public void onGroupCollapsed(int groupPosition) {
+    }
+
+    @Override
+    public long getCombinedChildId(long groupId, long childId) {
+        if (!mDataValid) return 0; 
+        return childId;
+    }
+
+    @Override
+    public long getCombinedGroupId(long groupId) {
+        if (!mDataValid) return 0; 
+        return groupId;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return !mDataValid || mCursor == null || mCursor.isClosed() || mCursor.getCount() == 0;
+    }
+}
diff --git a/src/com/android/browser/DeviceAccountLogin.java b/src/com/android/browser/DeviceAccountLogin.java
new file mode 100644
index 0000000..0638f97
--- /dev/null
+++ b/src/com/android/browser/DeviceAccountLogin.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2011 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
+import android.os.Bundle;
+import org.codeaurora.swe.WebView;
+
+public class DeviceAccountLogin implements
+        AccountManagerCallback<Bundle> {
+
+    private final Activity mActivity;
+    private final WebView mWebView;
+    private final Tab mTab;
+    private final WebViewController mWebViewController;
+    private final AccountManager mAccountManager;
+    Account[] mAccounts;
+    private AutoLoginCallback mCallback;
+    private String mAuthToken;
+
+    // Current state of the login.
+    private int mState = INITIAL;
+
+    public static final int INITIAL = 0;
+    public static final int FAILED = 1;
+    public static final int PROCESSING = 2;
+
+    public interface AutoLoginCallback {
+        public void loginFailed();
+    }
+
+    public DeviceAccountLogin(Activity activity, WebView view, Tab tab,
+            WebViewController controller) {
+        mActivity = activity;
+        mWebView = view;
+        mTab = tab;
+        mWebViewController = controller;
+        mAccountManager = AccountManager.get(activity);
+    }
+
+    public void handleLogin(String realm, String account, String args) {
+        mAccounts = mAccountManager.getAccountsByType(realm);
+        mAuthToken = "weblogin:" + args;
+
+        // No need to display UI if there are no accounts.
+        if (mAccounts.length == 0) {
+            return;
+        }
+
+        // Verify the account before using it.
+        for (Account a : mAccounts) {
+            if (a.name.equals(account)) {
+                // Handle the automatic login case where the service gave us an
+                // account to use.
+                mAccountManager.getAuthToken(a, mAuthToken, null,
+                       mActivity, this, null);
+                return;
+            }
+        }
+
+        displayLoginUi();
+    }
+
+    @Override
+    public void run(AccountManagerFuture<Bundle> value) {
+        try {
+            String result = value.getResult().getString(
+                    AccountManager.KEY_AUTHTOKEN);
+            if (result == null) {
+                loginFailed();
+            } else {
+                mWebView.loadUrl(result);
+                mTab.setDeviceAccountLogin(null);
+                if (mTab.inForeground()) {
+                    mWebViewController.hideAutoLogin(mTab);
+                }
+            }
+        } catch (Exception e) {
+            loginFailed();
+        }
+    }
+
+    public int getState() {
+        return mState;
+    }
+
+    private void loginFailed() {
+        mState = FAILED;
+        if (mTab.getDeviceAccountLogin() == null) {
+            displayLoginUi();
+        } else {
+            if (mCallback != null) {
+                mCallback.loginFailed();
+            }
+        }
+    }
+
+    private void displayLoginUi() {
+        // Display the account picker.
+        mTab.setDeviceAccountLogin(this);
+        if (mTab.inForeground()) {
+            mWebViewController.showAutoLogin(mTab);
+        }
+    }
+
+    public void cancel() {
+        mTab.setDeviceAccountLogin(null);
+    }
+
+    public void login(int accountIndex, AutoLoginCallback cb) {
+        mState = PROCESSING;
+        mCallback = cb;
+        mAccountManager.getAuthToken(
+                mAccounts[accountIndex], mAuthToken, null,
+                mActivity, this, null);
+    }
+
+    public String[] getAccountNames() {
+        String[] names = new String[mAccounts.length];
+        for (int i = 0; i < mAccounts.length; i++) {
+            names[i] = mAccounts[i].name;
+        }
+        return names;
+    }
+}
diff --git a/src/com/android/browser/DownloadHandler.java b/src/com/android/browser/DownloadHandler.java
new file mode 100644
index 0000000..65c5f85
--- /dev/null
+++ b/src/com/android/browser/DownloadHandler.java
@@ -0,0 +1,629 @@
+/*
+ * Copyright (C) 2010 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.app.AlertDialog;
+import android.app.DownloadManager;
+import android.app.DownloadManager.Request;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.StatFs;
+import android.os.storage.StorageManager;
+import android.util.Log;
+import org.codeaurora.swe.CookieManager;
+import android.webkit.URLUtil;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.WebAddress;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.File;
+/**
+ * Handle download requests
+ */
+public class DownloadHandler {
+
+    private static final boolean LOGD_ENABLED =
+            com.android.browser.Browser.LOGD_ENABLED;
+
+    private static final String LOGTAG = "DLHandler";
+    private static String mInternalStorage;
+    private static String mExternalStorage;
+    private final static String INVALID_PATH = "/storage";
+
+    public static void startingDownload(Activity activity,
+            String url, String userAgent, String contentDisposition,
+            String mimetype, String referer, boolean privateBrowsing, long contentLength,
+            String filename, String downloadPath) {
+        // java.net.URI is a lot stricter than KURL so we have to encode some
+        // extra characters. Fix for b 2538060 and b 1634719
+        WebAddress webAddress;
+        try {
+            webAddress = new WebAddress(url);
+            webAddress.setPath(encodePath(webAddress.getPath()));
+        } catch (Exception e) {
+            // This only happens for very bad urls, we want to chatch the
+            // exception here
+            Log.e(LOGTAG, "Exception trying to parse url:" + url);
+            return;
+        }
+
+        String addressString = webAddress.toString();
+        Uri uri = Uri.parse(addressString);
+        final DownloadManager.Request request;
+        try {
+            request = new DownloadManager.Request(uri);
+        } catch (IllegalArgumentException e) {
+            Toast.makeText(activity, R.string.cannot_download, Toast.LENGTH_SHORT).show();
+            return;
+        }
+        request.setMimeType(mimetype);
+        // set downloaded file destination to /sdcard/Download.
+        // or, should it be set to one of several Environment.DIRECTORY* dirs
+        // depending on mimetype?
+        try {
+            setDestinationDir(downloadPath, filename, request);
+        } catch (Exception e) {
+            showNoEnoughMemoryDialog(activity);
+            return;
+        }
+        // let this downloaded file be scanned by MediaScanner - so that it can
+        // show up in Gallery app, for example.
+        request.allowScanningByMediaScanner();
+        request.setDescription(webAddress.getHost());
+        // XXX: Have to use the old url since the cookies were stored using the
+        // old percent-encoded url.
+
+        String cookies = CookieManager.getInstance().getCookie(url, privateBrowsing);
+        request.addRequestHeader("cookie", cookies);
+        request.addRequestHeader("User-Agent", userAgent);
+        request.addRequestHeader("Referer", referer);
+        request.setNotificationVisibility(
+                DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED);
+        final DownloadManager manager = (DownloadManager) activity
+                .getSystemService(Context.DOWNLOAD_SERVICE);
+        new Thread("Browser download") {
+            public void run() {
+                manager.enqueue(request);
+            }
+        }.start();
+        showStartDownloadToast(activity);
+    }
+
+    private static boolean isAudioFileType(int fileType){
+        Object[] params  = {Integer.valueOf(fileType)};
+        Class[] type = new Class[] {int.class};
+        Boolean result = (Boolean) ReflectHelper.invokeStaticMethod("android.media.MediaFile",
+                               "isAudioFileType", type, params);
+        return result;
+    }
+
+    private static boolean isVideoFileType(int fileType){
+        Object[] params  = {Integer.valueOf(fileType)};
+        Class[] type = new Class[] {int.class};
+        Boolean result = (Boolean) ReflectHelper.invokeStaticMethod("android.media.MediaFile",
+                             "isVideoFileType", type, params);
+        return result;
+    }
+
+    /**
+     * Notify the host application a download should be done, or that
+     * the data should be streamed if a streaming viewer is available.
+     * @param activity Activity requesting the download.
+     * @param url The full url to the content that should be downloaded
+     * @param userAgent User agent of the downloading application.
+     * @param contentDisposition Content-disposition http header, if present.
+     * @param mimetype The mimetype of the content reported by the server
+     * @param referer The referer associated with the downloaded url
+     * @param privateBrowsing If the request is coming from a private browsing tab.
+     */
+    public static boolean onDownloadStart(final Activity activity, final String url,
+            final String userAgent, final String contentDisposition, final String mimetype,
+            final String referer, final boolean privateBrowsing, final long contentLength) {
+        // if we're dealing wih A/V content that's not explicitly marked
+        //     for download, check if it's streamable.
+        if (contentDisposition == null
+                || !contentDisposition.regionMatches(
+                        true, 0, "attachment", 0, 10)) {
+            // Add for Carrier Feature - When open an audio/video link, prompt a dialog
+            // to let the user choose play or download operation.
+            Uri uri = Uri.parse(url);
+            String scheme = uri.getScheme();
+            Log.v(LOGTAG, "scheme:" + scheme + ", mimetype:" + mimetype);
+            // Some mimetype for audio/video files is not started with "audio" or "video",
+            // such as ogg audio file with mimetype "application/ogg". So we also check
+            // file type by MediaFile.isAudioFileType() and MediaFile.isVideoFileType().
+            // For those file types other than audio or video, download it immediately.
+            Object[] params = {mimetype};
+            Class[] type = new Class[] {String.class};
+            Integer result = (Integer) ReflectHelper.invokeStaticMethod("android.media.MediaFile",
+                                           "getFileTypeForMimeType", type, params);
+            int fileType = result.intValue();
+            if ("http".equalsIgnoreCase(scheme) &&
+                    (mimetype.startsWith("audio/") ||
+                        mimetype.startsWith("video/") ||
+                            isAudioFileType(fileType) ||
+                                isVideoFileType(fileType))) {
+                new AlertDialog.Builder(activity)
+                .setTitle(R.string.application_name)
+                .setIcon(R.drawable.default_video_poster)
+                .setMessage(R.string.http_video_msg)
+                .setPositiveButton(R.string.video_save, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        onDownloadStartNoStream(activity, url, userAgent, contentDisposition,
+                                mimetype, referer, privateBrowsing, contentLength);
+                    }
+                 })
+                .setNegativeButton(R.string.video_play, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                        Intent intent = new Intent(Intent.ACTION_VIEW);
+                        intent.setDataAndType(Uri.parse(url), mimetype);
+                        try {
+                            String title = URLUtil.guessFileName(url, contentDisposition, mimetype);
+                            intent.putExtra(Intent.EXTRA_TITLE, title);
+                            activity.startActivity(intent);
+                        } catch (ActivityNotFoundException ex) {
+                            Log.w(LOGTAG, "When http stream play, activity not found for "
+                                    + mimetype + " over " + Uri.parse(url).getScheme(), ex);
+                        }
+                    }
+                }).show();
+
+                return true;
+            }
+            // query the package manager to see if there's a registered handler
+            //     that matches.
+            Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setDataAndType(Uri.parse(url), mimetype);
+            ResolveInfo info = activity.getPackageManager().resolveActivity(intent,
+                    PackageManager.MATCH_DEFAULT_ONLY);
+            if (info != null) {
+                ComponentName myName = activity.getComponentName();
+                // If we resolved to ourselves, we don't want to attempt to
+                // load the url only to try and download it again.
+                if (!myName.getPackageName().equals(
+                        info.activityInfo.packageName)
+                        || !myName.getClassName().equals(
+                                info.activityInfo.name)) {
+                    // someone (other than us) knows how to handle this mime
+                    // type with this scheme, don't download.
+                    try {
+                        activity.startActivity(intent);
+                        return false;
+                    } catch (ActivityNotFoundException ex) {
+                        if (LOGD_ENABLED) {
+                            Log.d(LOGTAG, "activity not found for " + mimetype
+                                    + " over " + Uri.parse(url).getScheme(),
+                                    ex);
+                        }
+                        // Best behavior is to fall back to a download in this
+                        // case
+                    }
+                }
+            }
+        }
+        onDownloadStartNoStream(activity, url, userAgent, contentDisposition,
+                mimetype, referer, privateBrowsing, contentLength);
+        return false;
+    }
+
+    // This is to work around the fact that java.net.URI throws Exceptions
+    // instead of just encoding URL's properly
+    // Helper method for onDownloadStartNoStream
+    private static String encodePath(String path) {
+        char[] chars = path.toCharArray();
+
+        boolean needed = false;
+        for (char c : chars) {
+            if (c == '[' || c == ']' || c == '|') {
+                needed = true;
+                break;
+            }
+        }
+        if (needed == false) {
+            return path;
+        }
+
+        StringBuilder sb = new StringBuilder("");
+        for (char c : chars) {
+            if (c == '[' || c == ']' || c == '|') {
+                sb.append('%');
+                sb.append(Integer.toHexString(c));
+            } else {
+                sb.append(c);
+            }
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Notify the host application a download should be done, even if there
+     * is a streaming viewer available for thise type.
+     * @param activity Activity requesting the download.
+     * @param url The full url to the content that should be downloaded
+     * @param userAgent User agent of the downloading application.
+     * @param contentDisposition Content-disposition http header, if present.
+     * @param mimetype The mimetype of the content reported by the server
+     * @param referer The referer associated with the downloaded url
+     * @param privateBrowsing If the request is coming from a private browsing tab.
+     */
+    /* package */static void onDownloadStartNoStream(Activity activity,
+            String url, String userAgent, String contentDisposition,
+            String mimetype, String referer, boolean privateBrowsing, long contentLength) {
+
+        initStorageDefaultPath(activity);
+        String filename = URLUtil.guessFileName(url,
+                contentDisposition, mimetype);
+
+        // Check to see if we have an SDCard
+        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 = activity.getString(R.string.download_sdcard_busy_dlg_msg);
+                title = R.string.download_sdcard_busy_dlg_title;
+            } else {
+                msg = activity.getString(R.string.download_no_sdcard_dlg_msg, filename);
+                title = R.string.download_no_sdcard_dlg_title;
+            }
+
+            new AlertDialog.Builder(activity)
+                .setTitle(title)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(msg)
+                .setPositiveButton(R.string.ok, null)
+                .show();
+            return;
+        }
+
+        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(activity, url, userAgent, referer,
+                    privateBrowsing, filename).start();
+        } else {
+            startDownloadSettings(activity, url, userAgent, contentDisposition, mimetype, referer,
+                    privateBrowsing, contentLength, filename);
+        }
+
+    }
+
+    public static void initStorageDefaultPath(Context context) {
+        mExternalStorage = getExternalStorageDirectory(context);
+        if (isPhoneStorageSupported()) {
+            mInternalStorage = Environment.getExternalStorageDirectory().getPath();
+        } else {
+            mInternalStorage = null;
+        }
+    }
+
+    public static void startDownloadSettings(Activity activity,
+            String url, String userAgent, String contentDisposition,
+            String mimetype, String referer, boolean privateBrowsing, long contentLength,
+            String filename) {
+        Bundle fileInfo = new Bundle();
+        fileInfo.putString("url", url);
+        fileInfo.putString("userAgent", userAgent);
+        fileInfo.putString("contentDisposition", contentDisposition);
+        fileInfo.putString("mimetype", mimetype);
+        fileInfo.putString("referer", referer);
+        fileInfo.putLong("contentLength", contentLength);
+        fileInfo.putBoolean("privateBrowsing", privateBrowsing);
+        fileInfo.putString("filename", filename);
+        Intent intent = new Intent("android.intent.action.BROWSERDOWNLOAD");
+        intent.putExtras(fileInfo);
+        activity.startActivity(intent);
+    }
+
+    public static void setAppointedFolder(String downloadPath) {
+        File file = new File(downloadPath);
+        if (file.exists()) {
+            if (!file.isDirectory()) {
+                throw new IllegalStateException(file.getAbsolutePath() +
+                        " already exists and is not a directory");
+            }
+        } else {
+            if (!file.mkdir()) {
+                throw new IllegalStateException("Unable to create directory: " +
+                        file.getAbsolutePath());
+            }
+        }
+    }
+
+    private static void setDestinationDir(String downloadPath, String filename, Request request) {
+        File file = new File(downloadPath);
+        if (file.exists()) {
+            if (!file.isDirectory()) {
+                throw new IllegalStateException(file.getAbsolutePath() +
+                        " already exists and is not a directory");
+            }
+        } else {
+            if (!file.mkdir()) {
+                throw new IllegalStateException("Unable to create directory: " +
+                        file.getAbsolutePath());
+            }
+        }
+        setDestinationFromBase(file, filename, request);
+    }
+
+    private static void setDestinationFromBase(File file, String filename, Request request) {
+        if (filename == null) {
+            throw new NullPointerException("filename cannot be null");
+        }
+        request.setDestinationUri(Uri.withAppendedPath(Uri.fromFile(file), filename));
+    }
+
+    public static void fileExistQueryDialog(Activity activity) {
+        new AlertDialog.Builder(activity)
+                .setTitle(R.string.download_file_exist)
+                .setIcon(android.R.drawable.ic_dialog_info)
+                .setMessage(R.string.download_file_exist_msg)
+                // if yes, delete existed file and start new download thread
+                .setPositiveButton(R.string.ok, null)
+                // if no, do nothing at all
+                .show();
+    }
+
+    public static long getAvailableMemory(String root) {
+        StatFs stat = new StatFs(root);
+        final long LEFT10MByte = 2560;
+        long blockSize = stat.getBlockSize();
+        long availableBlocks = stat.getAvailableBlocks() - LEFT10MByte;
+        return availableBlocks * blockSize;
+    }
+
+    public static void showNoEnoughMemoryDialog(Activity mContext) {
+        new AlertDialog.Builder(mContext)
+                .setTitle(R.string.download_no_enough_memory)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setMessage(R.string.download_no_enough_memory)
+                .setPositiveButton(R.string.ok, null)
+                .show();
+    }
+
+    public static boolean manageNoEnoughMemory(long contentLength, String root) {
+        long mAvailableBytes = getAvailableMemory(root);
+        if (mAvailableBytes > 0) {
+            if (contentLength > mAvailableBytes) {
+                return true;
+            }
+        } else {
+            return true;
+        }
+        return false;
+    }
+
+    public static void showStartDownloadToast(Activity activity) {
+        Intent intent = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+        activity.startActivity(intent);
+        Toast.makeText(activity, R.string.download_pending, Toast.LENGTH_SHORT)
+                .show();
+    }
+
+    /**
+     * wheather the storage status OK for download file
+     *
+     * @param activity
+     * @param filename the download file's name
+     * @param downloadPath the download file's path will be in
+     * @return boolean true is ok,and false is not
+     */
+    public static boolean isStorageStatusOK(Activity activity, String filename, String downloadPath) {
+        if (downloadPath.equals(INVALID_PATH)) {
+            new AlertDialog.Builder(activity)
+                    .setTitle(R.string.path_wrong)
+                    .setIcon(android.R.drawable.ic_dialog_alert)
+                    .setMessage(R.string.invalid_path)
+                    .setPositiveButton(R.string.ok, null)
+                    .show();
+            return false;
+        }
+
+        if (!(isPhoneStorageSupported() && downloadPath.contains(mInternalStorage))) {
+            String status = getExternalStorageState(activity);
+            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 = activity.getString(R.string.download_sdcard_busy_dlg_msg);
+                    title = R.string.download_sdcard_busy_dlg_title;
+                } else {
+                    msg = activity.getString(R.string.download_no_sdcard_dlg_msg, filename);
+                    title = R.string.download_no_sdcard_dlg_title;
+                }
+
+                new AlertDialog.Builder(activity)
+                        .setTitle(title)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(msg)
+                        .setPositiveButton(R.string.ok, null)
+                        .show();
+                return false;
+            }
+        } else {
+            String status = Environment.getExternalStorageState();
+            if (!status.equals(Environment.MEDIA_MOUNTED)) {
+                int mTitle = R.string.download_path_unavailable_dlg_title;
+                String mMsg = activity.getString(R.string.download_path_unavailable_dlg_msg);
+                new AlertDialog.Builder(activity)
+                        .setTitle(mTitle)
+                        .setIcon(android.R.drawable.ic_dialog_alert)
+                        .setMessage(mMsg)
+                        .setPositiveButton(R.string.ok, null)
+                        .show();
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * wheather support Phone Storage
+     *
+     * @return boolean true support Phone Storage ,false will be not
+     */
+    public static boolean isPhoneStorageSupported() {
+        return true;
+    }
+
+    /**
+     * show Dialog to warn filename is null
+     *
+     * @param activity
+     */
+    public static void showFilenameEmptyDialog(Activity activity) {
+        new AlertDialog.Builder(activity)
+                .setTitle(R.string.filename_empty_title)
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setMessage(R.string.filename_empty_msg)
+                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int which) {
+                    }
+                })
+                .show();
+    }
+
+    /**
+     * get the filename except the suffix and dot
+     *
+     * @return String the filename except suffix and dot
+     */
+    public static String getFilenameBase(String filename) {
+        int dotindex = filename.lastIndexOf('.');
+        if (dotindex != -1) {
+            return filename.substring(0, dotindex);
+        } else {
+            return "";
+        }
+    }
+
+    /**
+     * get the filename's extension from filename
+     *
+     * @param filename the download filename, may be the user entered
+     * @return String the filename's extension
+     */
+    public static String getFilenameExtension(String filename) {
+        int dotindex = filename.lastIndexOf('.');
+        if (dotindex != -1) {
+            return filename.substring(dotindex + 1);
+        } else {
+            return "";
+        }
+    }
+
+    public static String getDefaultDownloadPath(Context context) {
+        String defaultDownloadPath;
+
+        String defaultStorage;
+        if (isPhoneStorageSupported()) {
+            defaultStorage = Environment.getExternalStorageDirectory().getPath();
+        } else {
+            defaultStorage = getExternalStorageDirectory(context);
+        }
+
+        defaultDownloadPath = defaultStorage + context.getString(R.string.download_default_path);
+        Log.e(LOGTAG, "defaultStorage directory is : " + defaultDownloadPath);
+        return defaultDownloadPath;
+    }
+
+    /**
+     * translate the directory name into a name which is easy to know for user
+     *
+     * @param activity
+     * @param downloadPath
+     * @return String
+     */
+    public static String getDownloadPathForUser(Activity activity, String downloadPath) {
+        if (downloadPath == null) {
+            return downloadPath;
+        }
+        final String phoneStorageDir;
+        final String sdCardDir = getExternalStorageDirectory(activity);
+        if (isPhoneStorageSupported()) {
+            phoneStorageDir = Environment.getExternalStorageDirectory().getPath();
+        } else {
+            phoneStorageDir = null;
+        }
+
+        if (sdCardDir != null && downloadPath.startsWith(sdCardDir)) {
+            String sdCardLabel = activity.getResources().getString(
+                    R.string.download_path_sd_card_label);
+            downloadPath = downloadPath.replace(sdCardDir, sdCardLabel);
+        } else if ((phoneStorageDir != null) && downloadPath.startsWith(phoneStorageDir)) {
+            String phoneStorageLabel = activity.getResources().getString(
+                    R.string.download_path_phone_storage_label);
+            downloadPath = downloadPath.replace(phoneStorageDir, phoneStorageLabel);
+        }
+        return downloadPath;
+    }
+
+    private static boolean isRemovable(Object obj) {
+        return (Boolean) ReflectHelper.invokeMethod(obj,
+                "isRemovable", null, null);
+    }
+
+    private static boolean allowMassStorage(Object obj) {
+        return (Boolean) ReflectHelper.invokeMethod(obj,
+                "allowMassStorage", null, null);
+    }
+
+    private static String getPath(Object obj) {
+        return (String) ReflectHelper.invokeMethod(obj,
+                "getPath", null, null);
+    }
+
+    private static String getExternalStorageDirectory(Context context) {
+        String sd = null;
+        StorageManager mStorageManager = (StorageManager) context
+                .getSystemService(Context.STORAGE_SERVICE);
+        Object[] volumes = (Object[]) ReflectHelper.invokeMethod(
+                                 mStorageManager, "getVolumeList", null, null);
+        for (int i = 0; i < volumes.length; i++) {
+            if (isRemovable(volumes[i]) && allowMassStorage(volumes[i])) {
+                sd = getPath(volumes[i]);
+            }
+        }
+        return sd;
+    }
+
+    private static String getExternalStorageState(Context context) {
+        StorageManager mStorageManager = (StorageManager) context
+                .getSystemService(Context.STORAGE_SERVICE);
+        String path = getExternalStorageDirectory(context);
+        Object[] params  = {path};
+        Class[] type = new Class[] {String.class};
+        return (String) ReflectHelper.invokeMethod("android.os.storage.StorageManager",
+                "getVolumeState", type, params);
+    }
+}
diff --git a/src/com/android/browser/DownloadSettings.java b/src/com/android/browser/DownloadSettings.java
new file mode 100644
index 0000000..2b8a848
--- /dev/null
+++ b/src/com/android/browser/DownloadSettings.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *     * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser;
+
+import java.io.File;
+
+import android.app.Activity;
+import android.content.Intent;
+import java.lang.Thread;
+
+import com.android.browser.R;
+
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Environment;
+import android.text.format.*;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.view.Window;
+import android.widget.Toast;
+import android.text.TextUtils;
+
+public class DownloadSettings extends Activity {
+
+    private EditText downloadFilenameET;
+    private EditText downloadPathET;
+    private TextView downloadEstimateSize;
+    private TextView downloadEstimateTime;
+    private Button downloadStart;
+    private Button downloadCancel;
+    private String url;
+    private String userAgent;
+    private String contentDisposition;
+    private String mimetype;
+    private String referer;
+    private String filenameBase;
+    private String filename;
+    private String filenameExtension;
+    private boolean privateBrowsing;
+    private long contentLength;
+    private String downloadPath;
+    private String downloadPathForUser;
+    private static final int downloadRate = (1024 * 100 * 60);// Download Rate
+                                                              // 100KB/s
+    private final static String LOGTAG = "DownloadSettings";
+    private final static int DOWNLOAD_PATH = 0;
+    private boolean isDownloadStarted = false;
+
+    private static final String ENV_EMULATED_STORAGE_TARGET = "EMULATED_STORAGE_TARGET";
+
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // initial the DownloadSettings view
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.download_settings);
+        downloadFilenameET = (EditText) findViewById(R.id.download_filename_edit);
+        downloadPathET = (EditText) findViewById(R.id.download_filepath_selected);
+        downloadEstimateSize = (TextView) findViewById(R.id.download_estimate_size_content);
+        downloadEstimateTime = (TextView) findViewById(R.id.download_estimate_time_content);
+        downloadStart = (Button) findViewById(R.id.download_start);
+        downloadCancel = (Button) findViewById(R.id.download_cancle);
+        downloadPathET.setOnClickListener(downloadPathListener);
+        downloadStart.setOnClickListener(downloadStartListener);
+        downloadCancel.setOnClickListener(downloadCancelListener);
+
+        // get the bundle from Intent
+        Intent intent = getIntent();
+        Bundle fileInfo = intent.getExtras();
+        url = fileInfo.getString("url");
+        userAgent = fileInfo.getString("userAgent");
+        contentDisposition = fileInfo.getString("contentDisposition");
+        mimetype = fileInfo.getString("mimetype");
+        referer = fileInfo.getString("referer");
+        contentLength = fileInfo.getLong("contentLength");
+        privateBrowsing = fileInfo.getBoolean("privateBrowsing");
+        filename = fileInfo.getString("filename");
+
+        // download filenamebase's length is depended on filenameLength's values
+        // if filenamebase.length >= flienameLength, destroy the last string!
+
+        filenameBase = DownloadHandler.getFilenameBase(filename);
+        if (filenameBase.length() >= (BrowserUtils.FILENAME_MAX_LENGTH)) {
+            filenameBase = filenameBase.substring(0, BrowserUtils.FILENAME_MAX_LENGTH);
+        }
+
+        // warring when user enter more over letters into the EditText
+        BrowserUtils.maxLengthFilter(DownloadSettings.this, downloadFilenameET,
+                BrowserUtils.FILENAME_MAX_LENGTH);
+
+        downloadFilenameET.setText(filenameBase);
+        downloadPath = chooseFolderFromMimeType(BrowserSettings.getInstance().getDownloadPath(),
+                mimetype);
+        downloadPathForUser = DownloadHandler.getDownloadPathForUser(DownloadSettings.this,
+                downloadPath);
+        setDownloadPathForUserText(downloadPathForUser);
+        setDownloadFileSizeText();
+        setDownloadFileTimeText();
+
+    }
+
+    private OnClickListener downloadPathListener = new OnClickListener() {
+
+        @Override
+        public void onClick(View v) {
+
+            // start filemanager for getting download path
+            try {
+                Intent downloadPathIntent = new Intent("com.android.fileexplorer.action.DIR_SEL");
+                DownloadSettings.this.startActivityForResult(downloadPathIntent, DOWNLOAD_PATH);
+            } catch (Exception e) {
+                String err_msg = getString(R.string.activity_not_found,
+                        "com.android.fileexplorer.action.DIR_SEL");
+                Toast.makeText(DownloadSettings.this, err_msg, Toast.LENGTH_LONG).show();
+            }
+
+        }
+    };
+
+    private OnClickListener downloadStartListener = new OnClickListener() {
+
+        @Override
+        public void onClick(View v) {
+            filenameBase = getFilenameBaseFromUserEnter();
+            // check the filename user enter is null or not
+            if (filenameBase.length() <= 0) {
+                DownloadHandler.showFilenameEmptyDialog(DownloadSettings.this);
+                return;
+            }
+
+            filenameExtension = DownloadHandler.getFilenameExtension(filename);
+            filename = filenameBase + "." + filenameExtension;
+
+            // check the storage status
+            if (!DownloadHandler.isStorageStatusOK(DownloadSettings.this, filename, downloadPath)) {
+                return;
+            }
+
+            // check the storage memory enough or not
+            try {
+                DownloadHandler.setAppointedFolder(downloadPath);
+            } catch (Exception e) {
+                DownloadHandler.showNoEnoughMemoryDialog(DownloadSettings.this);
+                return;
+            }
+            boolean isNoEnoughMemory = DownloadHandler.manageNoEnoughMemory(contentLength,
+                    downloadPath);
+            if (isNoEnoughMemory) {
+                DownloadHandler.showNoEnoughMemoryDialog(DownloadSettings.this);
+                return;
+            }
+
+            // check the download file is exist or not
+            String fullFilename = downloadPath + "/" + filename;
+            if (mimetype != null && new File(fullFilename).exists()) {
+                DownloadHandler.fileExistQueryDialog(DownloadSettings.this);
+                return;
+            }
+
+            // staring downloading
+            DownloadHandler.startingDownload(DownloadSettings.this,
+                    url, userAgent, contentDisposition,
+                    mimetype, referer, privateBrowsing, contentLength,
+                    Uri.encode(filename), downloadPath);
+            isDownloadStarted = true;
+        }
+    };
+
+    private OnClickListener downloadCancelListener = new OnClickListener() {
+
+        @Override
+        public void onClick(View v) {
+            finish();
+        }
+    };
+
+    protected void onDestroy() {
+        super.onDestroy();
+    }
+
+    protected void onPause() {
+        super.onPause();
+        if (isDownloadStarted) {
+            finish();
+        }
+    }
+
+    protected void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+
+        if (DOWNLOAD_PATH == requestCode) {
+            if (resultCode == Activity.RESULT_OK && intent != null) {
+                downloadPath = intent.getStringExtra("result_dir_sel");
+                if (downloadPath != null) {
+                    String rawEmulatedStorageTarget = System.getenv(ENV_EMULATED_STORAGE_TARGET);
+                    if (!TextUtils.isEmpty(rawEmulatedStorageTarget)) {
+                        if (downloadPath.startsWith("/storage/sdcard0"))
+                            downloadPath = downloadPath.replace("/storage/sdcard0",
+                                    "/storage/emulated/0");
+                        if (downloadPath.startsWith("/storage/emulated/legacy"))
+                            downloadPath = downloadPath.replace("/storage/emulated/legacy",
+                                    "/storage/emulated/0");
+                    }
+                    downloadPathForUser = DownloadHandler.getDownloadPathForUser(
+                            DownloadSettings.this, downloadPath);
+                    setDownloadPathForUserText(downloadPathForUser);
+                }
+            }
+        }
+    }
+
+    // Add for carrier feature - download to related folders by mimetype.
+    private static String chooseFolderFromMimeType(String path, String mimeType) {
+        String destinationFolder = null;
+        if (!path.contains(Environment.DIRECTORY_DOWNLOADS) || null == mimeType)
+            return path;
+        if (mimeType.startsWith("audio"))
+            destinationFolder = Environment.DIRECTORY_MUSIC;
+        else if (mimeType.startsWith("video"))
+            destinationFolder = Environment.DIRECTORY_MOVIES;
+        else if (mimeType.startsWith("image"))
+            destinationFolder = Environment.DIRECTORY_PICTURES;
+        if (null != destinationFolder)
+            path = path.replace(Environment.DIRECTORY_DOWNLOADS, destinationFolder);
+        return path;
+    }
+
+    /**
+     * show download path for user
+     *
+     * @param downloadPath the download path user can see
+     */
+    private void setDownloadPathForUserText(String downloadPathForUser) {
+        downloadPathET.setText(downloadPathForUser);
+    }
+
+    /**
+     * get the filename from user select the download path
+     *
+     * @return String the filename from user selected
+     */
+    private String getFilenameBaseFromUserEnter() {
+        return downloadFilenameET.getText().toString();
+    }
+
+    /**
+     * set the download file size for user to be known
+     */
+    private void setDownloadFileSizeText() {
+        String sizeText;
+        if (contentLength <= 0) {
+            sizeText = getString(R.string.unknow_length);
+        } else {
+            sizeText = getDownloadFileSize();
+        }
+        downloadEstimateSize.setText(sizeText);
+
+    }
+
+    /**
+     * set the time which downloaded this file will be estimately use;
+     */
+    private void setDownloadFileTimeText() {
+        String neededTimeText;
+        if (contentLength <= 0) {
+            neededTimeText = getString(R.string.unknow_length);
+        } else {
+            neededTimeText = getNeededTime() + getString(R.string.time_min);
+        }
+        downloadEstimateTime.setText(neededTimeText);
+    }
+
+    /**
+     * count the download file's size and format the values
+     *
+     * @return String the format values
+     */
+    private String getDownloadFileSize() {
+        String currentSizeText = "";
+        if (contentLength > 0) {
+            currentSizeText = Formatter.formatFileSize(DownloadSettings.this, contentLength);
+        }
+        return currentSizeText;
+    }
+
+    /**
+     * get the time download this file will be use,and format this time values
+     *
+     * @return long the valses of time which download this file will be use
+     */
+    private long getNeededTime() {
+        long timeNeeded = contentLength / downloadRate;
+        if (timeNeeded < 1) {
+            timeNeeded = 1;
+        }
+        Log.e(LOGTAG, "TimeNeeded:" + timeNeeded + "min");
+        // return the time like 5 min, not 5 s;
+        return timeNeeded;
+    }
+}
diff --git a/src/com/android/browser/DownloadTouchIcon.java b/src/com/android/browser/DownloadTouchIcon.java
new file mode 100644
index 0000000..d2c4024
--- /dev/null
+++ b/src/com/android/browser/DownloadTouchIcon.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2009 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 org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.params.HttpClientParams;
+import org.apache.http.conn.params.ConnRouteParams;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Proxy;
+import android.net.http.AndroidHttpClient;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Message;
+
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Images;
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.WebView;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+class DownloadTouchIcon extends AsyncTask<String, Void, Void> {
+
+    private final ContentResolver mContentResolver;
+    private Cursor mCursor;
+    private final String mOriginalUrl;
+    private final String mUrl;
+    private final String mUserAgent; // Sites may serve a different icon to different UAs
+    private Message mMessage;
+
+    private final Context mContext;
+    /* package */ Tab mTab;
+
+    /**
+     * Use this ctor to store the touch icon in the bookmarks database for
+     * the originalUrl so we take account of redirects. Used when the user
+     * bookmarks a page from outside the bookmarks activity.
+     */
+    public DownloadTouchIcon(Tab tab, Context ctx, ContentResolver cr, WebView view) {
+        mTab = tab;
+        mContext = ctx.getApplicationContext();
+        mContentResolver = cr;
+        // Store these in case they change.
+        mOriginalUrl = view.getOriginalUrl();
+        mUrl = view.getUrl();
+        mUserAgent = view.getSettings().getUserAgentString();
+    }
+
+    /**
+     * Use this ctor to download the touch icon and update the bookmarks database
+     * entry for the given url. Used when the user creates a bookmark from
+     * within the bookmarks activity and there haven't been any redirects.
+     * TODO: Would be nice to set the user agent here so that there is no
+     * potential for the three different ctors here to return different icons.
+     */
+    public DownloadTouchIcon(Context ctx, ContentResolver cr, String url) {
+        mTab = null;
+        mContext = ctx.getApplicationContext();
+        mContentResolver = cr;
+        mOriginalUrl = null;
+        mUrl = url;
+        mUserAgent = null;
+    }
+
+    /**
+     * Use this ctor to not store the touch icon in a database, rather add it to
+     * the passed Message's data bundle with the key
+     * {@link BrowserContract.Bookmarks#TOUCH_ICON} and then send the message.
+     */
+    public DownloadTouchIcon(Context context, Message msg, String userAgent) {
+        mMessage = msg;
+        mContext = context.getApplicationContext();
+        mContentResolver = null;
+        mOriginalUrl = null;
+        mUrl = null;
+        mUserAgent = userAgent;
+    }
+
+    @Override
+    public Void doInBackground(String... values) {
+        if (mContentResolver != null) {
+            mCursor = Bookmarks.queryCombinedForUrl(mContentResolver,
+                    mOriginalUrl, mUrl);
+        }
+
+        boolean inDatabase = mCursor != null && mCursor.getCount() > 0;
+
+        String url = values[0];
+
+        if (inDatabase || mMessage != null) {
+            AndroidHttpClient client = null;
+            HttpGet request = null;
+
+            try {
+                client = AndroidHttpClient.newInstance(mUserAgent);
+                //HttpHost httpHost = Proxy.getPreferredHttpHost(mContext, url);
+                Object[] params  = { mContext, url};
+                Class[] type = new Class[] {android.content.Context.class, String.class};
+                HttpHost httpHost = (HttpHost) ReflectHelper.invokeStaticMethod(
+                    "android.net.Proxy", "getPreferredHttpHost",
+                    type, params);
+                if (httpHost != null) {
+                    ConnRouteParams.setDefaultProxy(client.getParams(), httpHost);
+                }
+
+                request = new HttpGet(url);
+
+                // Follow redirects
+                HttpClientParams.setRedirecting(client.getParams(), true);
+
+                HttpResponse response = client.execute(request);
+                if (response.getStatusLine().getStatusCode() == 200) {
+                    HttpEntity entity = response.getEntity();
+                    if (entity != null) {
+                        InputStream content = entity.getContent();
+                        if (content != null) {
+                            Bitmap icon = BitmapFactory.decodeStream(
+                                    content, null, null);
+                            if (inDatabase) {
+                                storeIcon(icon);
+                            } else if (mMessage != null) {
+                                Bundle b = mMessage.getData();
+                                b.putParcelable(BrowserContract.Bookmarks.TOUCH_ICON, icon);
+                            }
+                        }
+                    }
+                }
+            } catch (Exception ex) {
+                if (request != null) {
+                    request.abort();
+                }
+            } finally {
+                if (client != null) {
+                    client.close();
+                }
+            }
+        }
+
+        if (mCursor != null) {
+            mCursor.close();
+        }
+
+        if (mMessage != null) {
+            mMessage.sendToTarget();
+        }
+
+        return null;
+    }
+
+    @Override
+    protected void onCancelled() {
+        if (mCursor != null) {
+            mCursor.close();
+        }
+    }
+
+    private void storeIcon(Bitmap icon) {
+        // Do this first in case the download failed.
+        if (mTab != null) {
+            // Remove the touch icon loader from the BrowserActivity.
+            mTab.mTouchIconLoader = null;
+        }
+
+        if (icon == null || mCursor == null || isCancelled()) {
+            return;
+        }
+
+        if (mCursor.moveToFirst()) {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            icon.compress(Bitmap.CompressFormat.PNG, 100, os);
+
+            ContentValues values = new ContentValues();
+            values.put(Images.TOUCH_ICON, os.toByteArray());
+
+            do {
+                values.put(Images.URL, mCursor.getString(0));
+                mContentResolver.update(Images.CONTENT_URI, values, null, null);
+            } while (mCursor.moveToNext());
+        }
+    }
+}
diff --git a/src/com/android/browser/ErrorConsoleView.java b/src/com/android/browser/ErrorConsoleView.java
new file mode 100644
index 0000000..bcee7b5
--- /dev/null
+++ b/src/com/android/browser/ErrorConsoleView.java
@@ -0,0 +1,332 @@
+/*
+ * Copyright (C) 2009 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.Context;
+import android.database.DataSetObserver;
+import android.graphics.Color;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.View.OnClickListener;
+import android.webkit.ConsoleMessage;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.TwoLineListItem;
+
+import java.util.Vector;
+
+/* package */ class ErrorConsoleView extends LinearLayout {
+
+    /**
+     * Define some constants to describe the visibility of the error console.
+     */
+    public static final int SHOW_MINIMIZED = 0;
+    public static final int SHOW_MAXIMIZED = 1;
+    public static final int SHOW_NONE      = 2;
+
+    private TextView mConsoleHeader;
+    private ErrorConsoleListView mErrorList;
+    private LinearLayout mEvalJsViewGroup;
+    private EditText mEvalEditText;
+    private Button mEvalButton;
+    private WebView mWebView;
+    private int mCurrentShowState = SHOW_NONE;
+
+    private boolean mSetupComplete = false;
+
+    // Before we've been asked to display the console, cache any messages that should
+    // be added to the console. Then when we do display the console, add them to the view
+    // then.
+    private Vector<ConsoleMessage> mErrorMessageCache;
+
+    public ErrorConsoleView(Context context) {
+        super(context);
+    }
+
+    public ErrorConsoleView(Context context, AttributeSet attributes) {
+        super(context, attributes);
+    }
+
+    private void commonSetupIfNeeded() {
+        if (mSetupComplete) {
+            return;
+        }
+
+        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.error_console, this);
+
+        // Get references to each ui element.
+        mConsoleHeader = (TextView) findViewById(R.id.error_console_header_id);
+        mErrorList = (ErrorConsoleListView) findViewById(R.id.error_console_list_id);
+        mEvalJsViewGroup = (LinearLayout) findViewById(R.id.error_console_eval_view_group_id);
+        mEvalEditText = (EditText) findViewById(R.id.error_console_eval_text_id);
+        mEvalButton = (Button) findViewById(R.id.error_console_eval_button_id);
+
+        mEvalButton.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                // Send the javascript to be evaluated to webkit as a javascript: url
+                // TODO: Can we expose access to webkit's JS interpreter here and evaluate it that
+                // way? Note that this is called on the UI thread so we will need to post a message
+                // to the WebCore thread to implement this.
+                if (mWebView != null) {
+                    mWebView.loadUrl("javascript:" + mEvalEditText.getText());
+                }
+
+                mEvalEditText.setText("");
+            }
+        });
+
+        // Make clicking on the console title bar min/maximse it.
+        mConsoleHeader.setOnClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                if (mCurrentShowState == SHOW_MINIMIZED) {
+                    showConsole(SHOW_MAXIMIZED);
+                } else {
+                    showConsole(SHOW_MINIMIZED);
+                }
+            }
+        });
+
+        // Add any cached messages to the list now that we've assembled the view.
+        if (mErrorMessageCache != null) {
+            for (ConsoleMessage msg : mErrorMessageCache) {
+                mErrorList.addErrorMessage(msg);
+            }
+            mErrorMessageCache.clear();
+        }
+
+        mSetupComplete = true;
+    }
+
+    /**
+     * Adds a message to the set of messages the console uses.
+     */
+    public void addErrorMessage(ConsoleMessage consoleMessage) {
+        if (mSetupComplete) {
+            mErrorList.addErrorMessage(consoleMessage);
+        } else {
+            if (mErrorMessageCache == null) {
+                mErrorMessageCache = new Vector<ConsoleMessage>();
+            }
+            mErrorMessageCache.add(consoleMessage);
+        }
+    }
+
+    /**
+     * Removes all error messages from the console.
+     */
+    public void clearErrorMessages() {
+        if (mSetupComplete) {
+            mErrorList.clearErrorMessages();
+        } else if (mErrorMessageCache != null) {
+            mErrorMessageCache.clear();
+        }
+    }
+
+    /**
+     * Returns the current number of errors displayed in the console.
+     */
+    public int numberOfErrors() {
+        if (mSetupComplete) {
+            return mErrorList.getCount();
+        } else {
+            return (mErrorMessageCache == null) ? 0 : mErrorMessageCache.size();
+        }
+    }
+
+    /**
+     * Sets the webview that this console is associated with. Currently this is used so
+     * we can call into webkit to evaluate JS expressions in the console.
+     */
+    public void setWebView(WebView webview) {
+        mWebView = webview;
+    }
+
+    /**
+     * Sets the visibility state of the console.
+     */
+    public void showConsole(int show_state) {
+        commonSetupIfNeeded();
+        switch (show_state) {
+            case SHOW_MINIMIZED:
+                mConsoleHeader.setVisibility(View.VISIBLE);
+                mConsoleHeader.setText(R.string.error_console_header_text_minimized);
+                mErrorList.setVisibility(View.GONE);
+                mEvalJsViewGroup.setVisibility(View.GONE);
+                break;
+
+            case SHOW_MAXIMIZED:
+                mConsoleHeader.setVisibility(View.VISIBLE);
+                mConsoleHeader.setText(R.string.error_console_header_text_maximized);
+                mErrorList.setVisibility(View.VISIBLE);
+                mEvalJsViewGroup.setVisibility(View.VISIBLE);
+                break;
+
+            case SHOW_NONE:
+                mConsoleHeader.setVisibility(View.GONE);
+                mErrorList.setVisibility(View.GONE);
+                mEvalJsViewGroup.setVisibility(View.GONE);
+                break;
+        }
+        mCurrentShowState = show_state;
+    }
+
+    /**
+     * Returns the current visibility state of the console.
+     */
+    public int getShowState() {
+        if (mSetupComplete) {
+            return mCurrentShowState;
+        } else {
+            return SHOW_NONE;
+        }
+    }
+
+    /**
+     * This class extends ListView to implement the View that will actually display the set of
+     * errors encountered on the current page.
+     */
+    private static class ErrorConsoleListView extends ListView {
+        // An adapter for this View that contains a list of error messages.
+        private ErrorConsoleMessageList mConsoleMessages;
+
+        public ErrorConsoleListView(Context context, AttributeSet attributes) {
+            super(context, attributes);
+            mConsoleMessages = new ErrorConsoleMessageList(context);
+            setAdapter(mConsoleMessages);
+        }
+
+        public void addErrorMessage(ConsoleMessage consoleMessage) {
+            mConsoleMessages.add(consoleMessage);
+            setSelection(mConsoleMessages.getCount());
+        }
+
+        public void clearErrorMessages() {
+            mConsoleMessages.clear();
+        }
+
+        /**
+         * This class is an adapter for ErrorConsoleListView that contains the error console
+         * message data.
+         */
+        private static class ErrorConsoleMessageList extends android.widget.BaseAdapter
+                implements android.widget.ListAdapter {
+
+            private Vector<ConsoleMessage> mMessages;
+            private LayoutInflater mInflater;
+
+            public ErrorConsoleMessageList(Context context) {
+                mMessages = new Vector<ConsoleMessage>();
+                mInflater = (LayoutInflater)context.getSystemService(
+                        Context.LAYOUT_INFLATER_SERVICE);
+            }
+
+            /**
+             * Add a new message to the list and update the View.
+             */
+            public void add(ConsoleMessage consoleMessage) {
+                mMessages.add(consoleMessage);
+                notifyDataSetChanged();
+            }
+
+            /**
+             * Remove all messages from the list and update the view.
+             */
+            public void clear() {
+                mMessages.clear();
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public boolean areAllItemsEnabled() {
+                return false;
+            }
+
+            @Override
+            public boolean isEnabled(int position) {
+                return false;
+            }
+
+            public long getItemId(int position) {
+                return position;
+            }
+
+            public Object getItem(int position) {
+                return mMessages.get(position);
+            }
+
+            public int getCount() {
+                return mMessages.size();
+            }
+
+            @Override
+            public boolean hasStableIds() {
+                return true;
+            }
+
+            /**
+             * Constructs a TwoLineListItem for the error at position.
+             */
+            public View getView(int position, View convertView, ViewGroup parent) {
+                View view;
+                ConsoleMessage error = mMessages.get(position);
+
+                if (error == null) {
+                    return null;
+                }
+
+                if (convertView == null) {
+                    view = mInflater.inflate(android.R.layout.two_line_list_item, parent, false);
+                } else {
+                    view = convertView;
+                }
+
+                TextView headline = (TextView) view.findViewById(android.R.id.text1);
+                TextView subText = (TextView) view.findViewById(android.R.id.text2);
+                headline.setText(error.sourceId() + ":" + error.lineNumber());
+                subText.setText(error.message());
+                switch (error.messageLevel()) {
+                    case ERROR:
+                        subText.setTextColor(Color.RED);
+                        break;
+                    case WARNING:
+                        // Orange
+                        subText.setTextColor(Color.rgb(255,192,0));
+                        break;
+                    case TIP:
+                        subText.setTextColor(Color.BLUE);
+                        break;
+                    default:
+                        subText.setTextColor(Color.LTGRAY);
+                        break;
+                }
+                return view;
+            }
+
+        }
+    }
+}
diff --git a/src/com/android/browser/EventLogTags.logtags b/src/com/android/browser/EventLogTags.logtags
new file mode 100644
index 0000000..b3834cf
--- /dev/null
+++ b/src/com/android/browser/EventLogTags.logtags
@@ -0,0 +1,15 @@
+# See system/core/logcat/event.logtags for a description of the format of this file.
+
+option java_package com.android.browser
+
+# This event is logged when a user adds a new bookmark. This could just be a boolean,
+# but if lots of users add the same bookmark it could be a default bookmark on the browser.
+# Second parameter is where the bookmark was added from, currently history or bookmarks view.
+70103 browser_bookmark_added (url|3), (where|3)
+
+# This event is logged after a page has finished loading. It is sending back the page url,
+# and how long it took to load the page. Could maybe also tell the kind of connection (2g, 3g, WiFi)?
+70104 browser_page_loaded (url|3), (time|2|3)
+
+# This event is logged when the user navigates to a new page, sending the time spent on the current page.
+70105 browser_timeonpage (url|3), (time|2|3)
diff --git a/src/com/android/browser/FetchUrlMimeType.java b/src/com/android/browser/FetchUrlMimeType.java
new file mode 100644
index 0000000..f42d627
--- /dev/null
+++ b/src/com/android/browser/FetchUrlMimeType.java
@@ -0,0 +1,280 @@
+/*
+ * 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.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.MimeTypeMap;
+
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.IOException;
+
+import org.apache.http.Header;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.conn.params.ConnRouteParams;
+
+import org.codeaurora.swe.CookieManager;
+
+/**
+ * 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.
+ * 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 Thread {
+
+    private final static String LOGTAG = "FetchUrlMimeType";
+
+    private Context mContext;
+    private String mUri;
+    private String mUserAgent;
+    private String mFilename;
+    private String mReferer;
+    private Activity mActivity;
+    private boolean mPrivateBrowsing;
+    private long mContentLength;
+
+    public FetchUrlMimeType(Activity activity, String url, String userAgent,
+            String referer, boolean privateBrowsing, String filename) {
+        mActivity = activity;
+        mContext = activity.getApplicationContext();
+        mUri = url;
+        mUserAgent = userAgent;
+        mPrivateBrowsing = privateBrowsing;
+        mFilename = filename;
+        mReferer = referer;
+    }
+
+    @Override
+    public void run() {
+        // User agent is likely to be null, though the AndroidHttpClient
+        // seems ok with that.
+        AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
+        HttpHost httpHost;
+        try {
+            Class<?> argTypes[] = new Class[]{Context.class, String.class};
+            Object args[] = new Object[]{mContext, mUri};
+            httpHost = (HttpHost) ReflectHelper.invokeStaticMethod("android.net.Proxy",
+                "getPreferredHttpHost", argTypes, args);
+            if (httpHost != null) {
+                ConnRouteParams.setDefaultProxy(client.getParams(), httpHost);
+            }
+        } catch (IllegalArgumentException ex) {
+            Log.e(LOGTAG,"Download failed: " + ex);
+            client.close();
+            return;
+        }
+        HttpHead request = new HttpHead(mUri);
+        String cookies = CookieManager.getInstance().getCookie(mUri, mPrivateBrowsing);
+        if (cookies != null && cookies.length() > 0) {
+            request.addHeader("Cookie", cookies);
+        }
+
+        HttpResponse response;
+        String filename = mFilename;
+        String mimeType = null;
+        String contentDisposition = null;
+        String contentLength = 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);
+                    }
+                }
+                Header contentLengthHeader = response.getFirstHeader("Content-Length");
+                if (contentLengthHeader != null) {
+                    contentLength = contentLengthHeader.getValue();
+                }
+                Header contentDispositionHeader = response.getFirstHeader("Content-Disposition");
+                if (contentDispositionHeader != null) {
+                    contentDisposition = contentDispositionHeader.getValue();
+                }
+            }
+        } catch (IllegalArgumentException ex) {
+            request.abort();
+        } catch (IOException ex) {
+            request.abort();
+        } finally {
+            client.close();
+        }
+
+        if (mimeType != null) {
+            Log.e(LOGTAG, "-----------the mimeType from http header is ------------->" + mimeType);
+            if (mimeType.equalsIgnoreCase("text/plain") ||
+                    mimeType.equalsIgnoreCase("application/octet-stream")) {
+                String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+                        MimeTypeMap.getFileExtensionFromUrl(mUri));
+                if (newMimeType != null) {
+                    mimeType = newMimeType;
+                }
+            }
+
+            String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+            if (fileExtension == null || (fileExtension != null && fileExtension.equals("bin"))) {
+                fileExtension = MimeTypeMap.getFileExtensionFromUrl(mUri);
+                if (fileExtension == null) {
+                    fileExtension = "bin";
+                }
+            }
+            filename = DownloadHandler.getFilenameBase(filename) + "." + fileExtension;
+
+        } else {
+            String fileExtension = getFileExtensionFromUrlEx(mUri);
+            if (fileExtension == "") {
+                fileExtension = "bin";
+            }
+            String newMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension);
+            if (newMimeType != null) {
+                mimeType = newMimeType;
+            }
+            filename = guessFileNameEx(mUri, contentDisposition, mimeType);
+
+        }
+
+        if (contentLength != null) {
+            mContentLength = Long.parseLong(contentLength);
+        } else {
+            mContentLength = 0;
+        }
+
+        DownloadHandler.startDownloadSettings(mActivity, mUri, mUserAgent, contentDisposition,
+                mimeType, mReferer, mPrivateBrowsing, mContentLength, filename);
+    }
+
+    /**
+     * when we can not parse MineType and Filename from the header of http body
+     * ,Call the fallowing functions for this matter
+     * getFileExtensionFromUrlEx(String url) : get the file Extension from Url
+     * guessFileNameEx() : get the file name from url Note: this modified for
+     * download http://www.baidu.com girl picture error extension and error
+     * filename
+     */
+    private String getFileExtensionFromUrlEx(String url) {
+        Log.e("FetchUrlMimeType",
+                "--------can not get mimetype from http header, the URL is ---------->" + url);
+        if (!TextUtils.isEmpty(url)) {
+            int fragment = url.lastIndexOf('#');
+            if (fragment > 0) {
+                url = url.substring(0, fragment);
+            }
+
+            int filenamePos = url.lastIndexOf('/');
+            String filename =
+                    0 <= filenamePos ? url.substring(filenamePos + 1) : url;
+            Log.e(LOGTAG,
+                    "--------can not get mimetype from http header, the temp filename is----------"
+                            + filename);
+            // if the filename contains special characters, we don't
+            // consider it valid for our matching purposes:
+            if (!filename.isEmpty()) {
+                int dotPos = filename.lastIndexOf('.');
+                if (0 <= dotPos) {
+                    return filename.substring(dotPos + 1);
+                }
+            }
+        }
+
+        return "";
+    }
+
+    private String guessFileNameEx(String url, String contentDisposition, String mimeType) {
+        String filename = null;
+        String extension = null;
+
+        // If all the other http-related approaches failed, use the plain uri
+        if (filename == null) {
+            String decodedUrl = Uri.decode(url);
+            if (decodedUrl != null) {
+                if (!decodedUrl.endsWith("/")) {
+                    int index = decodedUrl.lastIndexOf('/') + 1;
+                    if (index > 0) {
+                        filename = decodedUrl.substring(index);
+                    }
+                }
+            }
+        }
+
+        // Finally, if couldn't get filename from URI, get a generic filename
+        if (filename == null) {
+            filename = "downloadfile";
+        }
+
+        // Split filename between base and extension
+        // Add an extension if filename does not have one
+        int dotIndex = filename.indexOf('.');
+        if (dotIndex < 0) {
+            if (mimeType != null) {
+                extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+                if (extension != null) {
+                    extension = "." + extension;
+                }
+            }
+            if (extension == null) {
+                if (mimeType != null && mimeType.toLowerCase().startsWith("text/")) {
+                    if (mimeType.equalsIgnoreCase("text/html")) {
+                        extension = ".html";
+                    } else {
+                        extension = ".txt";
+                    }
+                } else {
+                    extension = ".bin";
+                }
+            }
+        } else {
+            if (mimeType != null) {
+                // Compare the last segment of the extension against the mime
+                // type.
+                // If there's a mismatch, discard the entire extension.
+                int lastDotIndex = filename.lastIndexOf('.');
+                String typeFromExt = MimeTypeMap.getSingleton().getMimeTypeFromExtension(
+                        filename.substring(lastDotIndex + 1));
+                if (typeFromExt != null && !typeFromExt.equalsIgnoreCase(mimeType)) {
+                    extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
+                    if (extension != null) {
+                        extension = "." + extension;
+                    }
+                }
+            }
+            if (extension == null) {
+                extension = filename.substring(dotIndex);
+            }
+            filename = filename.substring(0, dotIndex);
+        }
+
+        return filename + extension;
+    }
+
+}
diff --git a/src/com/android/browser/GeolocationPermissionsPrompt.java b/src/com/android/browser/GeolocationPermissionsPrompt.java
new file mode 100755
index 0000000..127107f
--- /dev/null
+++ b/src/com/android/browser/GeolocationPermissionsPrompt.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2009 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 com.android.browser.R;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.webkit.GeolocationPermissions;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+public class GeolocationPermissionsPrompt extends RelativeLayout {
+    private TextView mMessage;
+    private Button mShareButton;
+    private Button mDontShareButton;
+    private CheckBox mRemember;
+    private GeolocationPermissions.Callback mCallback;
+    private String mOrigin;
+
+    public GeolocationPermissionsPrompt(Context context) {
+        this(context, null);
+    }
+
+    public GeolocationPermissionsPrompt(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        init();
+    }
+
+    private void init() {
+        mMessage = (TextView) findViewById(R.id.message);
+        mShareButton = (Button) findViewById(R.id.share_button);
+        mDontShareButton = (Button) findViewById(R.id.dont_share_button);
+        mRemember = (CheckBox) findViewById(R.id.remember);
+
+        mShareButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                handleButtonClick(true);
+            }
+        });
+        mDontShareButton.setOnClickListener(new View.OnClickListener() {
+            public void onClick(View v) {
+                handleButtonClick(false);
+            }
+        });
+    }
+
+    /**
+     * Shows the prompt for the given origin. When the user clicks on one of
+     * the buttons, the supplied callback is be called.
+     */
+    public void show(String origin, GeolocationPermissions.Callback callback) {
+        mOrigin = origin;
+        mCallback = callback;
+        Uri uri = Uri.parse(mOrigin);
+        setMessage("http".equals(uri.getScheme()) ?  mOrigin.substring(7) : mOrigin);
+        // The checkbox should always be intially checked.
+        mRemember.setChecked(true);
+        setVisibility(View.VISIBLE);
+    }
+
+    /**
+     * Hides the prompt.
+     */
+    public void hide() {
+        setVisibility(View.GONE);
+    }
+
+    /**
+     * Handles a click on one the buttons by invoking the callback.
+     */
+    private void handleButtonClick(boolean allow) {
+        hide();
+
+        boolean remember = mRemember.isChecked();
+        if (remember) {
+            Toast toast = Toast.makeText(
+                    getContext(),
+                    allow ? R.string.geolocation_permissions_prompt_toast_allowed :
+                            R.string.geolocation_permissions_prompt_toast_disallowed,
+                    Toast.LENGTH_LONG);
+            toast.setGravity(Gravity.BOTTOM, 0, 0);
+            toast.show();
+        }
+
+        mCallback.invoke(mOrigin, allow, remember);
+    }
+
+    /**
+     * Sets the prompt's message.
+     */
+    private void setMessage(CharSequence origin) {
+        mMessage.setText(String.format(
+            getResources().getString(R.string.geolocation_permissions_prompt_message),
+            origin));
+    }
+}
diff --git a/src/com/android/browser/GoogleAccountLogin.java b/src/com/android/browser/GoogleAccountLogin.java
new file mode 100644
index 0000000..f605671
--- /dev/null
+++ b/src/com/android/browser/GoogleAccountLogin.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2010 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.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.util.Log;
+import org.codeaurora.swe.CookieSyncManager;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebViewClient;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.util.EntityUtils;
+
+import com.android.browser.R;
+
+public class GoogleAccountLogin implements Runnable,
+        AccountManagerCallback<Bundle>, OnCancelListener {
+
+    private static final String LOGTAG = "BrowserLogin";
+
+    // Url for issuing the uber token.
+    private Uri ISSUE_AUTH_TOKEN_URL = Uri.parse(
+            "https://www.google.com/accounts/IssueAuthToken?service=gaia&Session=false");
+    // Url for signing into a particular service.
+    private static final Uri TOKEN_AUTH_URL = Uri.parse(
+            "https://www.google.com/accounts/TokenAuth");
+    // Google account type
+    private static final String GOOGLE = "com.google";
+    // Last auto login time
+    public static final String PREF_AUTOLOGIN_TIME = "last_autologin_time";
+
+    private final Activity mActivity;
+    private final Account mAccount;
+    private final WebView mWebView;
+    private Runnable mRunnable;
+    private ProgressDialog mProgressDialog;
+
+    // SID and LSID retrieval process.
+    private String mSid;
+    private String mLsid;
+    private int mState;  // {NONE(0), SID(1), LSID(2)}
+    private boolean mTokensInvalidated;
+    private String mUserAgent;
+
+    private GoogleAccountLogin(Activity activity, Account account,
+            Runnable runnable) {
+        mActivity = activity;
+        mAccount = account;
+        mWebView = new WebView(mActivity);
+        mRunnable = runnable;
+        mUserAgent = mWebView.getSettings().getUserAgentString();
+
+        // XXX: Doing pre-login causes onResume to skip calling
+        // resumeWebViewTimers. So to avoid problems with timers not running, we
+        // duplicate the work here using the off-screen WebView.
+        CookieSyncManager.getInstance().startSync();
+        WebViewTimersControl.getInstance().onBrowserActivityResume(mWebView);
+
+        mWebView.setWebViewClient(new WebViewClient() {
+            @Override
+            public boolean shouldOverrideUrlLoading(WebView view, String url) {
+                return false;
+            }
+            @Override
+            public void onPageFinished(WebView view, String url) {
+                done();
+            }
+        });
+    }
+
+    private void saveLoginTime() {
+        Editor ed = BrowserSettings.getInstance().getPreferences().edit();
+        ed.putLong(PREF_AUTOLOGIN_TIME, System.currentTimeMillis());
+        ed.apply();
+    }
+
+    // Runnable
+    @Override
+    public void run() {
+        String url = ISSUE_AUTH_TOKEN_URL.buildUpon()
+                .appendQueryParameter("SID", mSid)
+                .appendQueryParameter("LSID", mLsid)
+                .build().toString();
+        // Intentionally not using Proxy.
+        AndroidHttpClient client = AndroidHttpClient.newInstance(mUserAgent);
+        HttpPost request = new HttpPost(url);
+
+        String result = null;
+        try {
+            HttpResponse response = client.execute(request);
+            int status = response.getStatusLine().getStatusCode();
+            if (status != HttpStatus.SC_OK) {
+                Log.d(LOGTAG, "LOGIN_FAIL: Bad status from auth url "
+                      + status + ": "
+                      + response.getStatusLine().getReasonPhrase());
+                // Invalidate the tokens once just in case the 403 was for other
+                // reasons.
+                if (status == HttpStatus.SC_FORBIDDEN && !mTokensInvalidated) {
+                    Log.d(LOGTAG, "LOGIN_FAIL: Invalidating tokens...");
+                    // Need to regenerate the auth tokens and try again.
+                    invalidateTokens();
+                    // XXX: Do not touch any more member variables from this
+                    // thread as a second thread will handle the next login
+                    // attempt.
+                    return;
+                }
+                done();
+                return;
+            }
+            HttpEntity entity = response.getEntity();
+            if (entity == null) {
+                Log.d(LOGTAG, "LOGIN_FAIL: Null entity in response");
+                done();
+                return;
+            }
+            result = EntityUtils.toString(entity, "UTF-8");
+        } catch (Exception e) {
+            Log.d(LOGTAG, "LOGIN_FAIL: Exception acquiring uber token " + e);
+            request.abort();
+            done();
+            return;
+        } finally {
+            client.close();
+        }
+        final String newUrl = TOKEN_AUTH_URL.buildUpon()
+                .appendQueryParameter("source", "android-browser")
+                .appendQueryParameter("auth", result)
+                .appendQueryParameter("continue",
+                        BrowserSettings.getFactoryResetHomeUrl(mActivity))
+                .build().toString();
+        mActivity.runOnUiThread(new Runnable() {
+            @Override public void run() {
+                // Check mRunnable in case the request has been canceled.  This
+                // is most likely not necessary as run() is the only non-UI
+                // thread that calls done() but I am paranoid.
+                synchronized (GoogleAccountLogin.this) {
+                    if (mRunnable == null) {
+                        return;
+                    }
+                    mWebView.loadUrl(newUrl);
+                }
+            }
+        });
+    }
+
+    private void invalidateTokens() {
+        AccountManager am = AccountManager.get(mActivity);
+        am.invalidateAuthToken(GOOGLE, mSid);
+        am.invalidateAuthToken(GOOGLE, mLsid);
+        mTokensInvalidated = true;
+        mState = 1;  // SID
+        am.getAuthToken(mAccount, "SID", null, mActivity, this, null);
+    }
+
+    // AccountManager callbacks.
+    @Override
+    public void run(AccountManagerFuture<Bundle> value) {
+        try {
+            String id = value.getResult().getString(
+                    AccountManager.KEY_AUTHTOKEN);
+            switch (mState) {
+                default:
+                case 0:
+                    throw new IllegalStateException(
+                            "Impossible to get into this state");
+                case 1:
+                    mSid = id;
+                    mState = 2;  // LSID
+                    AccountManager.get(mActivity).getAuthToken(
+                            mAccount, "LSID", null, mActivity, this, null);
+                    break;
+                case 2:
+                    mLsid = id;
+                    new Thread(this).start();
+                    break;
+            }
+        } catch (Exception e) {
+            Log.d(LOGTAG, "LOGIN_FAIL: Exception in state " + mState + " " + e);
+            // For all exceptions load the original signin page.
+            // TODO: toast login failed?
+            done();
+        }
+    }
+
+    // Start the login process if auto-login is enabled and the user is not
+    // already logged in.
+    public static void startLoginIfNeeded(Activity activity,
+            Runnable runnable) {
+        // Already logged in?
+        if (isLoggedIn()) {
+            runnable.run();
+            return;
+        }
+
+        // No account found?
+        Account[] accounts = getAccounts(activity);
+        if (accounts == null || accounts.length == 0) {
+            runnable.run();
+            return;
+        }
+
+        GoogleAccountLogin login =
+                new GoogleAccountLogin(activity, accounts[0], runnable);
+        login.startLogin();
+    }
+
+    private void startLogin() {
+        saveLoginTime();
+        mProgressDialog = ProgressDialog.show(mActivity,
+                mActivity.getString(R.string.pref_autologin_title),
+                mActivity.getString(R.string.pref_autologin_progress,
+                                    mAccount.name),
+                true /* indeterminate */,
+                true /* cancelable */,
+                this);
+        mState = 1;  // SID
+        AccountManager.get(mActivity).getAuthToken(
+                mAccount, "SID", null, mActivity, this, null);
+    }
+
+    private static Account[] getAccounts(Context ctx) {
+        return AccountManager.get(ctx).getAccountsByType(GOOGLE);
+    }
+
+    // Checks if we already did pre-login.
+    private static boolean isLoggedIn() {
+        // See if we last logged in less than a week ago.
+        long lastLogin = BrowserSettings.getInstance().getPreferences()
+                .getLong(PREF_AUTOLOGIN_TIME, -1);
+        if (lastLogin == -1) {
+            return false;
+        }
+        return true;
+    }
+
+    // Used to indicate that the Browser should continue loading the main page.
+    // This can happen on success, error, or timeout.
+    private synchronized void done() {
+        if (mRunnable != null) {
+            Log.d(LOGTAG, "Finished login attempt for " + mAccount.name);
+            mActivity.runOnUiThread(mRunnable);
+
+            try {
+                mProgressDialog.dismiss();
+            } catch (Exception e) {
+                // TODO: Switch to a managed dialog solution (DialogFragment?)
+                // Also refactor this class, it doesn't
+                // play nice with the activity lifecycle, leading to issues
+                // with the dialog it manages
+                Log.w(LOGTAG, "Failed to dismiss mProgressDialog: " + e.getMessage());
+            }
+            mRunnable = null;
+            mActivity.runOnUiThread(new Runnable() {
+                @Override
+                public void run() {
+                    mWebView.destroy();
+                }
+            });
+        }
+    }
+
+    // Called by the progress dialog on startup.
+    public void onCancel(DialogInterface unused) {
+        done();
+    }
+
+}
diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java
new file mode 100644
index 0000000..ceba2cb
--- /dev/null
+++ b/src/com/android/browser/HistoryItem.java
@@ -0,0 +1,92 @@
+/*
+ * 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 com.android.browser.R;
+
+import android.content.Context;
+import android.provider.Browser;
+import android.view.View;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+
+/**
+ *  Layout representing a history item in the classic history viewer.
+ */
+/* package */ class HistoryItem extends BookmarkItem
+        implements OnCheckedChangeListener {
+
+    private CompoundButton  mStar;      // Star for bookmarking
+    /**
+     *  Create a new HistoryItem.
+     *  @param context  Context for this HistoryItem.
+     */
+    /* package */ HistoryItem(Context context) {
+        this(context, true);
+    }
+
+    /* package */ HistoryItem(Context context, boolean showStar) {
+        super(context);
+
+        mStar = (CompoundButton) findViewById(R.id.star);
+        mStar.setOnCheckedChangeListener(this);
+        if (showStar) {
+            mStar.setVisibility(View.VISIBLE);
+        } else {
+            mStar.setVisibility(View.GONE);
+        }
+    }
+    
+    /* package */ void copyTo(HistoryItem item) {
+        item.mTextView.setText(mTextView.getText());
+        item.mUrlText.setText(mUrlText.getText());
+        item.setIsBookmark(mStar.isChecked());
+        item.mImageView.setImageDrawable(mImageView.getDrawable());
+    }
+
+    /**
+     * Whether or not this item represents a bookmarked site
+     */
+    /* package */ boolean isBookmark() {
+        return mStar.isChecked();
+    }
+
+    /**
+     *  Set whether or not this represents a bookmark, and make sure the star
+     *  behaves appropriately.
+     */
+    /* package */ void setIsBookmark(boolean isBookmark) {
+        mStar.setOnCheckedChangeListener(null);
+        mStar.setChecked(isBookmark);
+        mStar.setOnCheckedChangeListener(this);
+    }
+
+    @Override
+    public void onCheckedChanged(CompoundButton buttonView,
+            boolean isChecked) {
+        if (isChecked) {
+            // Uncheck ourseves. When the bookmark is actually added,
+            // we will be notified
+            setIsBookmark(false);
+            Browser.saveBookmark(getContext(), getName(), mUrl);
+        } else {
+            Bookmarks.removeFromBookmarks(getContext(),
+                    getContext().getContentResolver(), mUrl, getName());
+        }
+    }
+}
diff --git a/src/com/android/browser/HttpAuthenticationDialog.java b/src/com/android/browser/HttpAuthenticationDialog.java
new file mode 100644
index 0000000..2981e65
--- /dev/null
+++ b/src/com/android/browser/HttpAuthenticationDialog.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2010 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 com.android.browser.R;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+/**
+ * HTTP authentication dialog.
+ */
+public class HttpAuthenticationDialog {
+
+    private final Context mContext;
+
+    private final String mHost;
+    private final String mRealm;
+
+    private AlertDialog mDialog;
+    private TextView mUsernameView;
+    private TextView mPasswordView;
+
+    private OkListener mOkListener;
+    private CancelListener mCancelListener;
+
+    /**
+     * Creates an HTTP authentication dialog.
+     */
+    public HttpAuthenticationDialog(Context context, String host, String realm) {
+        mContext = context;
+        mHost = host;
+        mRealm = realm;
+        createDialog();
+    }
+
+    private String getUsername() {
+        return mUsernameView.getText().toString();
+    }
+
+    private String getPassword() {
+        return mPasswordView.getText().toString();
+    }
+
+    /**
+     * Sets the listener that will be notified when the user submits the credentials.
+     */
+    public void setOkListener(OkListener okListener) {
+        mOkListener = okListener;
+    }
+
+    /**
+     * Sets the listener that will be notified when the user cancels the authentication
+     * dialog.
+     */
+    public void setCancelListener(CancelListener cancelListener) {
+        mCancelListener = cancelListener;
+    }
+
+    /**
+     * Shows the dialog.
+     */
+    public void show() {
+        mDialog.show();
+        mUsernameView.requestFocus();
+    }
+
+    /**
+     * Hides, recreates, and shows the dialog. This can be used to handle configuration changes.
+     */
+    public void reshow() {
+        String username = getUsername();
+        String password = getPassword();
+        int focusId = mDialog.getCurrentFocus().getId();
+        mDialog.dismiss();
+        createDialog();
+        mDialog.show();
+        if (username != null) {
+            mUsernameView.setText(username);
+        }
+        if (password != null) {
+            mPasswordView.setText(password);
+        }
+        if (focusId != 0) {
+            mDialog.findViewById(focusId).requestFocus();
+        } else {
+            mUsernameView.requestFocus();
+        }
+    }
+
+    private void createDialog() {
+        LayoutInflater factory = LayoutInflater.from(mContext);
+        View v = factory.inflate(R.layout.http_authentication, null);
+        mUsernameView = (TextView) v.findViewById(R.id.username_edit);
+        mPasswordView = (TextView) v.findViewById(R.id.password_edit);
+        mPasswordView.setOnEditorActionListener(new OnEditorActionListener() {
+            @Override
+            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                if (actionId == EditorInfo.IME_ACTION_DONE) {
+                    mDialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
+                    return true;
+                }
+                return false;
+            }
+        });
+
+        String title = mContext.getText(R.string.sign_in_to).toString().replace(
+                "%s1", mHost).replace("%s2", mRealm);
+
+        mDialog = new AlertDialog.Builder(mContext)
+                .setTitle(title)
+                .setIconAttribute(android.R.attr.alertDialogIcon)
+                .setView(v)
+                .setPositiveButton(R.string.action, new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        if (mOkListener != null) {
+                            mOkListener.onOk(mHost, mRealm, getUsername(), getPassword());
+                        }
+                    }})
+                .setNegativeButton(R.string.cancel,new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog, int whichButton) {
+                        if (mCancelListener != null) mCancelListener.onCancel();
+                    }})
+                .setOnCancelListener(new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        if (mCancelListener != null) mCancelListener.onCancel();
+                    }})
+                .create();
+
+        // Make the IME appear when the dialog is displayed if applicable.
+        mDialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
+    }
+
+    /**
+     * Interface for listeners that are notified when the user submits the credentials.
+     */
+    public interface OkListener {
+        void onOk(String host, String realm, String username, String password);
+    }
+
+    /**
+     * Interface for listeners that are notified when the user cancels the dialog.
+     */
+    public interface CancelListener {
+        void onCancel();
+    }
+}
diff --git a/src/com/android/browser/IntentHandler.java b/src/com/android/browser/IntentHandler.java
new file mode 100644
index 0000000..ec19246
--- /dev/null
+++ b/src/com/android/browser/IntentHandler.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2010 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.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.nfc.NfcAdapter;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.util.Patterns;
+
+import com.android.browser.UI.ComboViews;
+import com.android.browser.search.SearchEngine;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Handle all browser related intents
+ */
+public class IntentHandler {
+
+    // "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";
+
+    /* package */ static final UrlData EMPTY_URL_DATA = new UrlData(null);
+
+    private Activity mActivity;
+    private Controller mController;
+    private TabControl mTabControl;
+    private BrowserSettings mSettings;
+
+    public IntentHandler(Activity browser, Controller controller) {
+        mActivity = browser;
+        mController = controller;
+        mTabControl = mController.getTabControl();
+        mSettings = controller.getSettings();
+    }
+
+    void onNewIntent(Intent intent) {
+        Tab current = mTabControl.getCurrentTab();
+        // 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 (current == null) {
+            // Try to reset the tab in case the index was incorrect.
+            current = mTabControl.getTab(0);
+            if (current == null) {
+                // No tabs at all so just ignore this intent.
+                return;
+            }
+            mController.setActiveTab(current);
+        }
+        final String action = intent.getAction();
+        final int flags = intent.getFlags();
+        if (Intent.ACTION_MAIN.equals(action) ||
+                (flags & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0) {
+            // just resume the browser
+            return;
+        }
+        if (BrowserActivity.ACTION_SHOW_BOOKMARKS.equals(action)) {
+            mController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+            return;
+        }
+
+        // In case the SearchDialog is open.
+        ((SearchManager) mActivity.getSystemService(Context.SEARCH_SERVICE))
+                .stopSearch();
+        if (Intent.ACTION_VIEW.equals(action)
+                || NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)
+                || Intent.ACTION_SEARCH.equals(action)
+                || MediaStore.INTENT_ACTION_MEDIA_SEARCH.equals(action)
+                || Intent.ACTION_WEB_SEARCH.equals(action)) {
+            // If this was a search request (e.g. search query directly typed into the address bar),
+            // pass it on to the default web search provider.
+            if (handleWebSearchIntent(mActivity, mController, intent)) {
+                return;
+            }
+
+            UrlData urlData = getUrlDataFromIntent(intent);
+            if (urlData.isEmpty()) {
+                urlData = new UrlData(mSettings.getHomePage());
+            }
+
+            if (intent.getBooleanExtra(Browser.EXTRA_CREATE_NEW_TAB, false)
+                  || urlData.isPreloaded()) {
+                Tab t = mController.openTab(urlData);
+                return;
+            }
+            /*
+             * TODO: Don't allow javascript URIs
+             * 0) If this is a javascript: URI, *always* open a new tab
+             * 1) If the URL is already opened, switch to that tab
+             * 2-phone) Reuse tab with same appId
+             * 2-tablet) Open new tab
+             */
+            final String appId = intent
+                    .getStringExtra(Browser.EXTRA_APPLICATION_ID);
+            if (!TextUtils.isEmpty(urlData.mUrl) &&
+                    urlData.mUrl.startsWith("javascript:")) {
+                // Always open javascript: URIs in new tabs
+                mController.openTab(urlData);
+                return;
+            }
+            if (Intent.ACTION_VIEW.equals(action)
+                    && (appId != null)
+                    && appId.startsWith(mActivity.getPackageName())) {
+                Tab appTab = mTabControl.getTabFromAppId(appId);
+                if ((appTab != null) && (appTab == mController.getCurrentTab())) {
+                    mController.switchToTab(appTab);
+                    mController.loadUrlDataIn(appTab, urlData);
+                    return;
+                }
+            }
+            if (Intent.ACTION_VIEW.equals(action)
+                     && !mActivity.getPackageName().equals(appId)) {
+                if (!BrowserActivity.isTablet(mActivity)
+                        && !mSettings.allowAppTabs()) {
+                    Tab appTab = mTabControl.getTabFromAppId(appId);
+                    if (appTab != null) {
+                        mController.reuseTab(appTab, urlData);
+                        return;
+                    }
+                }
+                // No matching application tab, try to find a regular tab
+                // with a matching url.
+                Tab appTab = mTabControl.findTabWithUrl(urlData.mUrl);
+                if (appTab != null) {
+                    // Transfer ownership
+                    appTab.setAppId(appId);
+                    if (current != appTab) {
+                        mController.switchToTab(appTab);
+                    }
+                    // Otherwise, we are already viewing the correct tab.
+                } else {
+                    // if FLAG_ACTIVITY_BROUGHT_TO_FRONT flag is on, the url
+                    // will be 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.
+                    Tab tab = mController.openTab(urlData);
+                    if (tab != null) {
+                        tab.setAppId(appId);
+                        if ((intent.getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
+                            tab.setCloseOnBack(true);
+                        }
+                    }
+                }
+            } else {
+                if (!urlData.isEmpty()
+                        && urlData.mUrl.startsWith("about:debug")) {
+                    if ("about:debug.dom".equals(urlData.mUrl)) {
+                        current.getWebView().dumpDomTree(false);
+                    } else if ("about:debug.dom.file".equals(urlData.mUrl)) {
+                        current.getWebView().dumpDomTree(true);
+                    } else if ("about:debug.render".equals(urlData.mUrl)) {
+                        current.getWebView().dumpRenderTree(false);
+                    } else if ("about:debug.render.file".equals(urlData.mUrl)) {
+                        current.getWebView().dumpRenderTree(true);
+                    } else if ("about:debug.display".equals(urlData.mUrl)) {
+                        current.getWebView().dumpDisplayTree();
+                    } else if ("about:debug.nav".equals(urlData.mUrl)) {
+                        current.getWebView().debugDump();
+                    } else {
+                        mSettings.toggleDebugSettings();
+                    }
+                    return;
+                }
+                // Get rid of the subwindow if it exists
+                mController.dismissSubWindow(current);
+                // If the current Tab is being used as an application tab,
+                // remove the association, since the new Intent means that it is
+                // no longer associated with that application.
+                current.setAppId(null);
+                mController.loadUrlDataIn(current, urlData);
+            }
+        }
+    }
+
+    protected static UrlData getUrlDataFromIntent(Intent intent) {
+        String url = "";
+        Map<String, String> headers = null;
+        PreloadedTabControl preloaded = null;
+        String preloadedSearchBoxQuery = null;
+        if (intent != null
+                && (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) == 0) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_VIEW.equals(action) ||
+                    NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
+                url = UrlUtils.smartUrlFilter(intent.getData());
+                if (url != null && url.startsWith("http")) {
+                    final Bundle pairs = intent
+                            .getBundleExtra(Browser.EXTRA_HEADERS);
+                    if (pairs != null && !pairs.isEmpty()) {
+                        Iterator<String> iter = pairs.keySet().iterator();
+                        headers = new HashMap<String, String>();
+                        while (iter.hasNext()) {
+                            String key = iter.next();
+                            headers.put(key, pairs.getString(key));
+                        }
+                    }
+                }
+                if (intent.hasExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID)) {
+                    String id = intent.getStringExtra(PreloadRequestReceiver.EXTRA_PRELOAD_ID);
+                    preloadedSearchBoxQuery = intent.getStringExtra(
+                            PreloadRequestReceiver.EXTRA_SEARCHBOX_SETQUERY);
+                    preloaded = Preloader.getInstance().getPreloadedTab(id);
+                }
+            } 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);
+                if (url != null) {
+                    // In general, we shouldn't modify URL from Intent.
+                    // But currently, we get the user-typed URL from search box as well.
+                    url = UrlUtils.fixUrl(url);
+                    url = UrlUtils.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("source");
+                        }
+                        if (TextUtils.isEmpty(source)) {
+                            source = GOOGLE_SEARCH_SOURCE_UNKNOWN;
+                        }
+                        url = url.replace(searchSource, "&source=android-"+source+"&");
+                    }
+                }
+            }
+        }
+        return new UrlData(url, headers, intent, preloaded, preloadedSearchBoxQuery);
+    }
+
+    /**
+     * Launches the default web search activity with the query parameters if the given intent's data
+     * are identified as plain search terms and not URLs/shortcuts.
+     * @return true if the intent was handled and web search activity was launched, false if not.
+     */
+    static boolean handleWebSearchIntent(Activity activity,
+            Controller controller, Intent intent) {
+        if (intent == null) return false;
+
+        String url = null;
+        final String action = intent.getAction();
+        if (Intent.ACTION_VIEW.equals(action)) {
+            Uri data = intent.getData();
+            if (data != null) url = data.toString();
+        } 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);
+        }
+        return handleWebSearchRequest(activity, controller, url,
+                intent.getBundleExtra(SearchManager.APP_DATA),
+                intent.getStringExtra(SearchManager.EXTRA_DATA_KEY));
+    }
+
+    /**
+     * Launches the default web search activity with the query parameters if the given url string
+     * was identified as plain search terms and not URL/shortcut.
+     * @return true if the request was handled and web search activity was launched, false if not.
+     */
+    private static boolean handleWebSearchRequest(Activity activity,
+            Controller controller, String inUrl, Bundle appData,
+            String extraData) {
+        if (inUrl == null) return false;
+
+        // In general, we shouldn't modify URL from Intent.
+        // But currently, we get the user-typed URL from search box as well.
+        String url = UrlUtils.fixUrl(inUrl).trim();
+        if (TextUtils.isEmpty(url)) return false;
+
+        // URLs are handled by the regular flow of control, so
+        // return early.
+        if (Patterns.WEB_URL.matcher(url).matches()
+                || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) {
+            return false;
+        }
+
+        final ContentResolver cr = activity.getContentResolver();
+        final String newUrl = url;
+        if (controller == null || controller.getTabControl() == null
+                || controller.getTabControl().getCurrentWebView() == null
+                || !controller.getTabControl().getCurrentWebView()
+                .isPrivateBrowsingEnabled()) {
+            new AsyncTask<Void, Void, Void>() {
+                @Override
+                protected Void doInBackground(Void... unused) {
+                        Browser.addSearchUrl(cr, newUrl);
+                    return null;
+                }
+            }.execute();
+        }
+
+        SearchEngine searchEngine = BrowserSettings.getInstance().getSearchEngine();
+        if (searchEngine == null) return false;
+        searchEngine.startSearch(activity, url, appData, extraData);
+
+        return true;
+    }
+
+    /**
+     * A UrlData class to abstract how the content will be set to WebView.
+     * This base class uses loadUrl to show the content.
+     */
+    static class UrlData {
+        final String mUrl;
+        final Map<String, String> mHeaders;
+        final PreloadedTabControl mPreloadedTab;
+        final String mSearchBoxQueryToSubmit;
+        final boolean mDisableUrlOverride;
+
+        UrlData(String url) {
+            this.mUrl = url;
+            this.mHeaders = null;
+            this.mPreloadedTab = null;
+            this.mSearchBoxQueryToSubmit = null;
+            this.mDisableUrlOverride = false;
+        }
+
+        UrlData(String url, Map<String, String> headers, Intent intent) {
+            this(url, headers, intent, null, null);
+        }
+
+        UrlData(String url, Map<String, String> headers, Intent intent,
+                PreloadedTabControl preloaded, String searchBoxQueryToSubmit) {
+            this.mUrl = url;
+            this.mHeaders = headers;
+            this.mPreloadedTab = preloaded;
+            this.mSearchBoxQueryToSubmit = searchBoxQueryToSubmit;
+            if (intent != null) {
+                mDisableUrlOverride = intent.getBooleanExtra(
+                        BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, false);
+            } else {
+                mDisableUrlOverride = false;
+            }
+        }
+
+        boolean isEmpty() {
+            return (mUrl == null || mUrl.length() == 0);
+        }
+
+        boolean isPreloaded() {
+            return mPreloadedTab != null;
+        }
+
+        PreloadedTabControl getPreloadedTab() {
+            return mPreloadedTab;
+        }
+
+        String getSearchBoxQueryToSubmit() {
+            return mSearchBoxQueryToSubmit;
+        }
+    }
+
+}
diff --git a/src/com/android/browser/KeyChainLookup.java b/src/com/android/browser/KeyChainLookup.java
new file mode 100644
index 0000000..5bd86b5
--- /dev/null
+++ b/src/com/android/browser/KeyChainLookup.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 201 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.Context;
+import android.os.AsyncTask;
+import android.security.KeyChain;
+import android.security.KeyChainException;
+import org.codeaurora.swe.ClientCertRequestHandler;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+final class KeyChainLookup extends AsyncTask<Void, Void, Void> {
+    private final Context mContext;
+    private final ClientCertRequestHandler mHandler;
+    private final String mAlias;
+    KeyChainLookup(Context context, ClientCertRequestHandler handler, String alias) {
+        mContext = context.getApplicationContext();
+        mHandler = handler;
+        mAlias = alias;
+    }
+    @Override protected Void doInBackground(Void... params) {
+        PrivateKey privateKey;
+        X509Certificate[] certificateChain;
+        try {
+            privateKey = KeyChain.getPrivateKey(mContext, mAlias);
+            certificateChain = KeyChain.getCertificateChain(mContext, mAlias);
+        } catch (InterruptedException e) {
+            mHandler.ignore();
+            return null;
+        } catch (KeyChainException e) {
+            mHandler.ignore();
+            return null;
+        }
+        mHandler.proceed(privateKey, certificateChain);
+        return null;
+    }
+}
diff --git a/src/com/android/browser/LogTag.java b/src/com/android/browser/LogTag.java
new file mode 100644
index 0000000..b2393c7
--- /dev/null
+++ b/src/com/android/browser/LogTag.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2010 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.EventLog;
+
+public class LogTag {
+
+    public static final int BROWSER_BOOKMARK_ADDED = 70103;
+    public static final int BROWSER_PAGE_LOADED = 70104;
+    public static final int BROWSER_TIMEONPAGE = 70105;
+    /**
+     * Log when the user is adding a new bookmark.
+     *
+     * @param url the url of the new bookmark.
+     * @param where the location from where the bookmark was added
+     */
+    public static void logBookmarkAdded(String url, String where) {
+        EventLog.writeEvent(BROWSER_BOOKMARK_ADDED, url + "|"
+            + where);
+    }
+
+    /**
+     * Log when a page has finished loading with how much
+     * time the browser used to load the page.
+     *
+     * Note that a redirect will restart the timer, so this time is not
+     * always how long it takes for the user to load a page.
+     *
+     * @param url the url of that page that finished loading.
+     * @param duration the time the browser spent loading the page.
+     */
+    public static void logPageFinishedLoading(String url, long duration) {
+        EventLog.writeEvent(BROWSER_PAGE_LOADED, url + "|"
+            + duration);
+    }
+
+    /**
+     * log the time the user has spent on a webpage
+     *
+     * @param url the url of the page that is being logged (old page).
+     * @param duration the time spent on the webpage.
+     */
+    public static void logTimeOnPage(String url, long duration) {
+        EventLog.writeEvent(BROWSER_TIMEONPAGE, url + "|"
+            + duration);
+    }
+}
diff --git a/src/com/android/browser/MemoryMonitor.java b/src/com/android/browser/MemoryMonitor.java
new file mode 100644
index 0000000..a18f698
--- /dev/null
+++ b/src/com/android/browser/MemoryMonitor.java
@@ -0,0 +1,133 @@
+/*
+    * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+    *
+    * Redistribution and use in source and binary forms, with or without
+    * modification, are permitted provided that the following conditions are
+    * met:
+    * * Redistributions of source code must retain the above copyright
+    * notice, this list of conditions and the following disclaimer.
+    * * Redistributions in binary form must reproduce the above
+    * copyright notice, this list of conditions and the following
+    * disclaimer in the documentation and/or other materials provided
+    * with the distribution.
+    * * Neither the name of The Linux Foundation nor the names of its
+    * contributors may be used to endorse or promote products derived
+    * from this software without specific prior written permission.
+    *
+    * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+    * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+    * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+    * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+    * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+    * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+    * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+    * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+    * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+    *
+    */
+
+package com.android.browser;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.util.Log;
+import java.sql.Timestamp;
+
+public class MemoryMonitor {
+
+    //This number is used with device memory class to calculate max number
+    //of active tabs.
+    private static int sMaxActiveTabs = 0;
+    private static MemoryMonitor sMemoryMonitor;
+    private TabControl mTabControl;
+    private final static String LOGTAG = "MemoryMonitor";
+
+    // Should be called only once
+
+    public static MemoryMonitor getInstance(Context context,
+                                            Controller controller) {
+        if (sMemoryMonitor == null) {
+            sMemoryMonitor = new MemoryMonitor(context,controller);
+        }
+        return sMemoryMonitor;
+    }
+
+    MemoryMonitor(Context context,Controller controller) {
+        mTabControl = controller.getTabControl();
+        sMaxActiveTabs = getMaxActiveTabs(context);
+        Log.d(LOGTAG,"Max Active Tabs: "+ sMaxActiveTabs);
+    }
+
+    private int getActiveTabs() {
+        int numNativeActiveTab = 0;
+        int size = mTabControl.getTabCount();
+
+        for (int i = 0; i < size; i++) {
+           Tab tab =  mTabControl.getTab(i);
+           if (((Tab)tab).isNativeActive()){
+                numNativeActiveTab++;
+           }
+        }
+        return numNativeActiveTab;
+    }
+
+    /**
+      * if number of tabs whose native tab is active, is greater
+      * than MAX_ACTIVE_TABS destroy the nativetab of oldest used Tab
+      */
+
+    public void destroyLeastRecentlyActiveTab() {
+        int numActiveTabs = getActiveTabs();
+        int numActiveTabsToRelease = numActiveTabs - sMaxActiveTabs;
+
+        // The most common case will be that we need to delete one
+        // NativeTab to make room for a new one.  So, find the most-stale.
+        if (numActiveTabsToRelease == 1) {
+            Tab mostStaleTab = null;
+            for (Tab t : mTabControl.getTabs()) {
+                if (t.isNativeActive() && !(t.inForeground())) {
+                    if (mostStaleTab == null){
+                        mostStaleTab = t;
+                    }
+                    else {
+                        if (t.getTimestamp().compareTo(mostStaleTab.
+                            getTimestamp()) < 0) {
+                            mostStaleTab = t;
+                        }
+                    }
+                }
+            }
+            if (mostStaleTab != null) {
+                mostStaleTab.destroy();
+           }
+        } else if (numActiveTabsToRelease > 1) {
+            // Since there is more than 1 "extra" tab, just release all
+            // NativeTabs in the background. This would be true when
+            // tracking was turned on after multiple tabs already exists
+            for (Tab t : mTabControl.getTabs()) {
+                if (t.isNativeActive() && !(t.inForeground())) {
+                    t.destroy();
+                }
+            }
+        }
+    }
+
+    /**
+      * Returns the default max number of active tabs based on device's
+      * memory class.
+      */
+    static int getMaxActiveTabs(Context context) {
+        // We use device memory class to decide number of active tabs
+        // (minimum memory class is 16).
+        ActivityManager am =(ActivityManager)context.
+            getSystemService(Context.ACTIVITY_SERVICE);
+        if (am.getMemoryClass() < 33) {
+            return 1;   // only 1 Tab can be active at a time
+        }
+        else {
+            return 2;  // atleast 2 Tabs can be active at a time
+        }
+    }
+}
diff --git a/src/com/android/browser/MessagesReceiver.java b/src/com/android/browser/MessagesReceiver.java
new file mode 100644
index 0000000..d59ae84
--- /dev/null
+++ b/src/com/android/browser/MessagesReceiver.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following
+ *      disclaimer in the documentation and/or other materials provided
+ *      with the distribution.
+ *    * Neither the name of The Linux Foundation nor the names of its
+ *      contributors may be used to endorse or promote products derived
+ *      from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser;
+
+import org.w3c.dom.Text;
+
+import com.android.browser.R;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+public class MessagesReceiver extends BroadcastReceiver {
+    private static final String TAG = "MessagesReceiver";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        Log.d(TAG, "onReceive: " + intent.getAction());
+        if ((intent == null) || TextUtils.isEmpty(intent.getStringExtra("from"))) {
+            return;
+        }
+
+        if (BrowserSettings.getInstance().useFullscreen()) {
+            String from = intent.getStringExtra("from");
+            Log.d(TAG, "the message from: " + from);
+            Toast.makeText(context, context.getString(R.string.received_message_full_screen, from),
+                    Toast.LENGTH_LONG).show();
+        }
+    }
+}
diff --git a/src/com/android/browser/NavScreen.java b/src/com/android/browser/NavScreen.java
new file mode 100644
index 0000000..42b35de
--- /dev/null
+++ b/src/com/android/browser/NavScreen.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2011 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.Configuration;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.NavTabScroller.OnLayoutListener;
+import com.android.browser.NavTabScroller.OnRemoveListener;
+import com.android.browser.TabControl.OnThumbnailUpdatedListener;
+import com.android.browser.UI.ComboViews;
+
+import java.util.HashMap;
+
+public class NavScreen extends RelativeLayout
+        implements OnClickListener, OnMenuItemClickListener, OnThumbnailUpdatedListener {
+
+
+    UiController mUiController;
+    PhoneUi mUi;
+    Tab mTab;
+    Activity mActivity;
+
+    ImageButton mRefresh;
+    ImageButton mForward;
+    ImageButton mBookmarks;
+    ImageButton mMore;
+    ImageButton mNewTab;
+    FrameLayout mHolder;
+
+    TextView mTitle;
+    ImageView mFavicon;
+    ImageButton mCloseTab;
+
+    NavTabScroller mScroller;
+    TabAdapter mAdapter;
+    int mOrientation;
+    boolean mNeedsMenu;
+    HashMap<Tab, View> mTabViews;
+
+    public NavScreen(Activity activity, UiController ctl, PhoneUi ui) {
+        super(activity);
+        mActivity = activity;
+        mUiController = ctl;
+        mUi = ui;
+        mOrientation = activity.getResources().getConfiguration().orientation;
+        init();
+    }
+
+    protected void showMenu() {
+        PopupMenu popup = new PopupMenu(getContext(), mMore);
+        Menu menu = popup.getMenu();
+        popup.getMenuInflater().inflate(R.menu.browser, menu);
+        mUiController.updateMenuState(mUiController.getCurrentTab(), menu);
+        popup.setOnMenuItemClickListener(this);
+        popup.show();
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        return mUiController.onOptionsItemSelected(item);
+    }
+
+    protected float getToolbarHeight() {
+        return mActivity.getResources().getDimension(R.dimen.toolbar_height);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newconfig) {
+        if (newconfig.orientation != mOrientation) {
+            int sv = mScroller.getScrollValue();
+            removeAllViews();
+            mOrientation = newconfig.orientation;
+            init();
+            mScroller.setScrollValue(sv);
+            mAdapter.notifyDataSetChanged();
+        }
+    }
+
+    public void refreshAdapter() {
+        mScroller.handleDataChanged(
+                mUiController.getTabControl().getTabPosition(mUi.getActiveTab()));
+    }
+
+    private void init() {
+        LayoutInflater.from(getContext()).inflate(R.layout.nav_screen, this);
+        setContentDescription(getContext().getResources().getString(
+                R.string.accessibility_transition_navscreen));
+        mBookmarks = (ImageButton) findViewById(R.id.bookmarks);
+        mNewTab = (ImageButton) findViewById(R.id.newtab);
+        mMore = (ImageButton) findViewById(R.id.more);
+        mBookmarks.setOnClickListener(this);
+        mNewTab.setOnClickListener(this);
+        mMore.setOnClickListener(this);
+        mScroller = (NavTabScroller) findViewById(R.id.scroller);
+        TabControl tc = mUiController.getTabControl();
+        mTabViews = new HashMap<Tab, View>(tc.getTabCount());
+        mAdapter = new TabAdapter(getContext(), tc);
+        mScroller.setOrientation(mOrientation == Configuration.ORIENTATION_LANDSCAPE
+                ? LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
+        // update state for active tab
+        mScroller.setAdapter(mAdapter,
+                mUiController.getTabControl().getTabPosition(mUi.getActiveTab()));
+        mScroller.setOnRemoveListener(new OnRemoveListener() {
+            public void onRemovePosition(int pos) {
+                Tab tab = mAdapter.getItem(pos);
+                onCloseTab(tab);
+            }
+        });
+        mNeedsMenu = !ViewConfiguration.get(getContext()).hasPermanentMenuKey();
+        if (!mNeedsMenu) {
+            mMore.setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mBookmarks == v) {
+            mUiController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+        } else if (mNewTab == v) {
+            openNewTab();
+        } else if (mMore == v) {
+            showMenu();
+        }
+    }
+
+    private void onCloseTab(Tab tab) {
+        if (tab != null) {
+            if (tab == mUiController.getCurrentTab()) {
+                mUiController.closeCurrentTab();
+            } else {
+                mUiController.closeTab(tab);
+            }
+        }
+    }
+
+    private void openNewTab() {
+        // need to call openTab explicitely with setactive false
+        final Tab tab = mUiController.openTab(BrowserSettings.getInstance().getHomePage(),
+                false, false, false);
+        if (tab != null) {
+            mUiController.setBlockEvents(true);
+            final int tix = mUi.mTabControl.getTabPosition(tab);
+            mScroller.setOnLayoutListener(new OnLayoutListener() {
+
+                @Override
+                public void onLayout(int l, int t, int r, int b) {
+                    mUi.hideNavScreen(tix, true);
+                    switchToTab(tab);
+                }
+            });
+            mScroller.handleDataChanged(tix);
+            mUiController.setBlockEvents(false);
+        }
+    }
+
+    private void switchToTab(Tab tab) {
+        if (tab != mUi.getActiveTab()) {
+            mUiController.setActiveTab(tab);
+        }
+    }
+
+    protected void close(int position) {
+        close(position, true);
+    }
+
+    protected void close(int position, boolean animate) {
+        mUi.hideNavScreen(position, animate);
+    }
+
+    protected NavTabView getTabView(int pos) {
+        return mScroller.getTabView(pos);
+    }
+
+    class TabAdapter extends BaseAdapter {
+
+        Context context;
+        TabControl tabControl;
+
+        public TabAdapter(Context ctx, TabControl tc) {
+            context = ctx;
+            tabControl = tc;
+        }
+
+        @Override
+        public int getCount() {
+            return tabControl.getTabCount();
+        }
+
+        @Override
+        public Tab getItem(int position) {
+            return tabControl.getTab(position);
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(final int position, View convertView, ViewGroup parent) {
+            final NavTabView tabview = new NavTabView(mActivity);
+            final Tab tab = getItem(position);
+            tabview.setWebView(tab);
+            mTabViews.put(tab, tabview.mImage);
+            tabview.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    if (tabview.isClose(v)) {
+                        mScroller.animateOut(tabview);
+                    } else if (tabview.isTitle(v)) {
+                        switchToTab(tab);
+                        mUi.getTitleBar().setSkipTitleBarAnimations(true);
+                        close(position, false);
+                        mUi.editUrl(false, true);
+                        mUi.getTitleBar().setSkipTitleBarAnimations(false);
+                    } else if (tabview.isWebView(v)) {
+                        close(position);
+                    }
+                }
+            });
+            return tabview;
+        }
+
+    }
+
+    @Override
+    public void onThumbnailUpdated(Tab t) {
+        View v = mTabViews.get(t);
+        if (v != null) {
+            v.invalidate();
+        }
+    }
+
+}
diff --git a/src/com/android/browser/NavTabScroller.java b/src/com/android/browser/NavTabScroller.java
new file mode 100644
index 0000000..a23ebe9
--- /dev/null
+++ b/src/com/android/browser/NavTabScroller.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2011 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
+
+import com.android.browser.view.ScrollerView;
+
+/**
+ * custom view for displaying tabs in the nav screen
+ */
+public class NavTabScroller extends ScrollerView {
+
+    static final int INVALID_POSITION = -1;
+    static final float[] PULL_FACTOR = { 2.5f, 0.9f };
+
+    interface OnRemoveListener {
+        public void onRemovePosition(int position);
+    }
+
+    interface OnLayoutListener {
+        public void onLayout(int l, int t, int r, int b);
+    }
+
+    private ContentLayout mContentView;
+    private BaseAdapter mAdapter;
+    private OnRemoveListener mRemoveListener;
+    private OnLayoutListener mLayoutListener;
+    private int mGap;
+    private int mGapPosition;
+    private ObjectAnimator mGapAnimator;
+
+    // after drag animation velocity in pixels/sec
+    private static final float MIN_VELOCITY = 1500;
+    private AnimatorSet mAnimator;
+
+    private float mFlingVelocity;
+    private boolean mNeedsScroll;
+    private int mScrollPosition;
+
+    DecelerateInterpolator mCubic;
+    int mPullValue;
+
+    public NavTabScroller(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    public NavTabScroller(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public NavTabScroller(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        mCubic = new DecelerateInterpolator(1.5f);
+        mGapPosition = INVALID_POSITION;
+        setHorizontalScrollBarEnabled(false);
+        setVerticalScrollBarEnabled(false);
+        mContentView = new ContentLayout(ctx, this);
+        mContentView.setOrientation(LinearLayout.HORIZONTAL);
+        addView(mContentView);
+        mContentView.setLayoutParams(
+                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+        // ProGuard !
+        setGap(getGap());
+        mFlingVelocity = getContext().getResources().getDisplayMetrics().density
+                * MIN_VELOCITY;
+    }
+
+    protected int getScrollValue() {
+        return mHorizontal ? getScrollX() : getScrollY();
+    }
+
+    protected void setScrollValue(int value) {
+        scrollTo(mHorizontal ? value : 0, mHorizontal ? 0 : value);
+    }
+
+    protected NavTabView getTabView(int pos) {
+        return (NavTabView) mContentView.getChildAt(pos);
+    }
+
+    protected boolean isHorizontal() {
+        return mHorizontal;
+    }
+
+    public void setOrientation(int orientation) {
+        mContentView.setOrientation(orientation);
+        if (orientation == LinearLayout.HORIZONTAL) {
+            mContentView.setLayoutParams(
+                    new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+        } else {
+            mContentView.setLayoutParams(
+                    new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
+        }
+        super.setOrientation(orientation);
+    }
+
+    @Override
+    protected void onMeasure(int wspec, int hspec) {
+        super.onMeasure(wspec, hspec);
+        calcPadding();
+    }
+
+    private void calcPadding() {
+        if (mAdapter.getCount() > 0) {
+            View v = mContentView.getChildAt(0);
+            if (mHorizontal) {
+                int pad = (getMeasuredWidth() - v.getMeasuredWidth()) / 2 + 2;
+                mContentView.setPadding(pad, 0, pad, 0);
+            } else {
+                int pad = (getMeasuredHeight() - v.getMeasuredHeight()) / 2 + 2;
+                mContentView.setPadding(0, pad, 0, pad);
+            }
+        }
+    }
+
+    public void setAdapter(BaseAdapter adapter) {
+        setAdapter(adapter, 0);
+    }
+
+
+    public void setOnRemoveListener(OnRemoveListener l) {
+        mRemoveListener = l;
+    }
+
+    public void setOnLayoutListener(OnLayoutListener l) {
+        mLayoutListener = l;
+    }
+
+    protected void setAdapter(BaseAdapter adapter, int selection) {
+        mAdapter = adapter;
+        mAdapter.registerDataSetObserver(new DataSetObserver() {
+
+            @Override
+            public void onChanged() {
+                super.onChanged();
+                handleDataChanged();
+            }
+
+            @Override
+            public void onInvalidated() {
+                super.onInvalidated();
+            }
+        });
+        handleDataChanged(selection);
+    }
+
+    protected ViewGroup getContentView() {
+        return mContentView;
+    }
+
+    protected int getRelativeChildTop(int ix) {
+        return mContentView.getChildAt(ix).getTop() - getScrollY();
+    }
+
+    protected void handleDataChanged() {
+        handleDataChanged(INVALID_POSITION);
+    }
+
+    void handleDataChanged(int newscroll) {
+        int scroll = getScrollValue();
+        if (mGapAnimator != null) {
+            mGapAnimator.cancel();
+        }
+        mContentView.removeAllViews();
+        for (int i = 0; i < mAdapter.getCount(); i++) {
+            View v = mAdapter.getView(i, null, mContentView);
+            LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                    LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+            lp.gravity = (mHorizontal ? Gravity.CENTER_VERTICAL : Gravity.CENTER_HORIZONTAL);
+            mContentView.addView(v, lp);
+            if (mGapPosition > INVALID_POSITION){
+                adjustViewGap(v, i);
+            }
+        }
+        if (newscroll > INVALID_POSITION) {
+            newscroll = Math.min(mAdapter.getCount() - 1, newscroll);
+            mNeedsScroll = true;
+            mScrollPosition = newscroll;
+            requestLayout();
+        } else {
+            setScrollValue(scroll);
+        }
+    }
+
+    protected void finishScroller() {
+        mScroller.forceFinished(true);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        if (mNeedsScroll) {
+            mScroller.forceFinished(true);
+            snapToSelected(mScrollPosition, false);
+            mNeedsScroll = false;
+        }
+        if (mLayoutListener != null) {
+            mLayoutListener.onLayout(l, t, r, b);
+            mLayoutListener = null;
+        }
+    }
+
+    void clearTabs() {
+        mContentView.removeAllViews();
+    }
+
+    void snapToSelected(int pos, boolean smooth) {
+        if (pos < 0) return;
+        View v = mContentView.getChildAt(pos);
+        if (v == null) return;
+        int sx = 0;
+        int sy = 0;
+        if (mHorizontal) {
+            sx = (v.getLeft() + v.getRight() - getWidth()) / 2;
+        } else {
+            sy = (v.getTop() + v.getBottom() - getHeight()) / 2;
+        }
+        if ((sx != getScrollX()) || (sy != getScrollY())) {
+            if (smooth) {
+                smoothScrollTo(sx,sy);
+            } else {
+                scrollTo(sx, sy);
+            }
+        }
+    }
+
+    protected void animateOut(View v) {
+        if (v == null) return;
+        animateOut(v, -mFlingVelocity);
+    }
+
+    private void animateOut(final View v, float velocity) {
+        float start = mHorizontal ? v.getTranslationY() : v.getTranslationX();
+        animateOut(v, velocity, start);
+    }
+
+    private void animateOut(final View v, float velocity, float start) {
+        if ((v == null) || (mAnimator != null)) return;
+        final int position = mContentView.indexOfChild(v);
+        int target = 0;
+        if (velocity < 0) {
+            target = mHorizontal ? -getHeight() :  -getWidth();
+        } else {
+            target = mHorizontal ? getHeight() : getWidth();
+        }
+        int distance = target - (mHorizontal ? v.getTop() : v.getLeft());
+        long duration = (long) (Math.abs(distance) * 1000 / Math.abs(velocity));
+        int scroll = 0;
+        int translate = 0;
+        int gap = mHorizontal ? v.getWidth() : v.getHeight();
+        int centerView = getViewCenter(v);
+        int centerScreen = getScreenCenter();
+        int newpos = INVALID_POSITION;
+        if (centerView < centerScreen - gap / 2) {
+            // top view
+            scroll = - (centerScreen - centerView - gap);
+            translate = (position > 0) ? gap : 0;
+            newpos = position;
+        } else if (centerView > centerScreen + gap / 2) {
+            // bottom view
+            scroll = - (centerScreen + gap - centerView);
+            if (position < mAdapter.getCount() - 1) {
+                translate = -gap;
+            }
+        } else {
+            // center view
+            scroll = - (centerScreen - centerView);
+            if (position < mAdapter.getCount() - 1) {
+                translate = -gap;
+            } else {
+                scroll -= gap;
+            }
+        }
+        mGapPosition = position;
+        final int pos = newpos;
+        ObjectAnimator trans = ObjectAnimator.ofFloat(v,
+                (mHorizontal ? TRANSLATION_Y : TRANSLATION_X), start, target);
+        ObjectAnimator alpha = ObjectAnimator.ofFloat(v, ALPHA, getAlpha(v,start),
+                getAlpha(v,target));
+        AnimatorSet set1 = new AnimatorSet();
+        set1.playTogether(trans, alpha);
+        set1.setDuration(duration);
+        mAnimator = new AnimatorSet();
+        ObjectAnimator trans2 = null;
+        ObjectAnimator scroll1 = null;
+        if (scroll != 0) {
+            if (mHorizontal) {
+                scroll1 = ObjectAnimator.ofInt(this, "scrollX", getScrollX(), getScrollX() + scroll);
+            } else {
+                scroll1 = ObjectAnimator.ofInt(this, "scrollY", getScrollY(), getScrollY() + scroll);
+            }
+        }
+        if (translate != 0) {
+            trans2 = ObjectAnimator.ofInt(this, "gap", 0, translate);
+        }
+        final int duration2 = 200;
+        if (scroll1 != null) {
+            if (trans2 != null) {
+                AnimatorSet set2 = new AnimatorSet();
+                set2.playTogether(scroll1, trans2);
+                set2.setDuration(duration2);
+                mAnimator.playSequentially(set1, set2);
+            } else {
+                scroll1.setDuration(duration2);
+                mAnimator.playSequentially(set1, scroll1);
+            }
+        } else {
+            if (trans2 != null) {
+                trans2.setDuration(duration2);
+                mAnimator.playSequentially(set1, trans2);
+            }
+        }
+        mAnimator.addListener(new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator a) {
+                if (mRemoveListener !=  null) {
+                    mRemoveListener.onRemovePosition(position);
+                    mAnimator = null;
+                    mGapPosition = INVALID_POSITION;
+                    mGap = 0;
+                    handleDataChanged(pos);
+                }
+            }
+        });
+        mAnimator.start();
+    }
+
+    public void setGap(int gap) {
+        if (mGapPosition != INVALID_POSITION) {
+            mGap = gap;
+            postInvalidate();
+        }
+    }
+
+    public int getGap() {
+        return mGap;
+    }
+
+    void adjustGap() {
+        for (int i = 0; i < mContentView.getChildCount(); i++) {
+            final View child = mContentView.getChildAt(i);
+            adjustViewGap(child, i);
+        }
+    }
+
+    private void adjustViewGap(View view, int pos) {
+        if ((mGap < 0 && pos > mGapPosition)
+                || (mGap > 0 && pos < mGapPosition)) {
+            if (mHorizontal) {
+                view.setTranslationX(mGap);
+            } else {
+                view.setTranslationY(mGap);
+            }
+        }
+    }
+
+    private int getViewCenter(View v) {
+        if (mHorizontal) {
+            return v.getLeft() + v.getWidth() / 2;
+        } else {
+            return v.getTop() + v.getHeight() / 2;
+        }
+    }
+
+    private int getScreenCenter() {
+        if (mHorizontal) {
+            return getScrollX() + getWidth() / 2;
+        } else {
+            return getScrollY() + getHeight() / 2;
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if (mGapPosition > INVALID_POSITION) {
+            adjustGap();
+        }
+        super.draw(canvas);
+    }
+
+    @Override
+    protected View findViewAt(int x, int y) {
+        x += getScrollX();
+        y += getScrollY();
+        final int count = mContentView.getChildCount();
+        for (int i = count - 1; i >= 0; i--) {
+            View child = mContentView.getChildAt(i);
+            if (child.getVisibility() == View.VISIBLE) {
+                if ((x >= child.getLeft()) && (x < child.getRight())
+                        && (y >= child.getTop()) && (y < child.getBottom())) {
+                    return child;
+                }
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void onOrthoDrag(View v, float distance) {
+        if ((v != null) && (mAnimator == null)) {
+            offsetView(v, distance);
+        }
+    }
+
+    @Override
+    protected void onOrthoDragFinished(View downView) {
+        if (mAnimator != null) return;
+        if (mIsOrthoDragged && downView != null) {
+            // offset
+            float diff = mHorizontal ? downView.getTranslationY() : downView.getTranslationX();
+            if (Math.abs(diff) > (mHorizontal ? downView.getHeight() : downView.getWidth()) / 2) {
+                // remove it
+                animateOut(downView, Math.signum(diff) * mFlingVelocity, diff);
+            } else {
+                // snap back
+                offsetView(downView, 0);
+            }
+        }
+    }
+
+    @Override
+    protected void onOrthoFling(View v, float velocity) {
+        if (v == null) return;
+        if (mAnimator == null && Math.abs(velocity) > mFlingVelocity / 2) {
+            animateOut(v, velocity);
+        } else {
+            offsetView(v, 0);
+        }
+    }
+
+    private void offsetView(View v, float distance) {
+        v.setAlpha(getAlpha(v, distance));
+        if (mHorizontal) {
+            v.setTranslationY(distance);
+        } else {
+            v.setTranslationX(distance);
+        }
+    }
+
+    private float getAlpha(View v, float distance) {
+        return 1 - (float) Math.abs(distance) / (mHorizontal ? v.getHeight() : v.getWidth());
+    }
+
+    private float ease(DecelerateInterpolator inter, float value, float start,
+            float dist, float duration) {
+        return start + dist * inter.getInterpolation(value / duration);
+    }
+
+    @Override
+    protected void onPull(int delta) {
+        boolean layer = false;
+        int count = 2;
+        if (delta == 0 && mPullValue == 0) return;
+        if (delta == 0 && mPullValue != 0) {
+            // reset
+            for (int i = 0; i < count; i++) {
+                View child = mContentView.getChildAt((mPullValue < 0)
+                        ? i
+                        : mContentView.getChildCount() - 1 - i);
+                if (child == null) break;
+                ObjectAnimator trans = ObjectAnimator.ofFloat(child,
+                        mHorizontal ? "translationX" : "translationY",
+                                mHorizontal ? getTranslationX() : getTranslationY(),
+                                0);
+                ObjectAnimator rot = ObjectAnimator.ofFloat(child,
+                        mHorizontal ? "rotationY" : "rotationX",
+                                mHorizontal ? getRotationY() : getRotationX(),
+                                0);
+                AnimatorSet set = new AnimatorSet();
+                set.playTogether(trans, rot);
+                set.setDuration(100);
+                set.start();
+            }
+            mPullValue = 0;
+        } else {
+            if (mPullValue == 0) {
+                layer = true;
+            }
+            mPullValue += delta;
+        }
+        final int height = mHorizontal ? getWidth() : getHeight();
+        int oscroll = Math.abs(mPullValue);
+        int factor = (mPullValue <= 0) ? 1 : -1;
+        for (int i = 0; i < count; i++) {
+            View child = mContentView.getChildAt((mPullValue < 0)
+                    ? i
+                    : mContentView.getChildCount() - 1 - i);
+            if (child == null) break;
+            if (layer) {
+            }
+            float k = PULL_FACTOR[i];
+            float rot = -factor * ease(mCubic, oscroll, 0, k * 2, height);
+            int y =  factor * (int) ease(mCubic, oscroll, 0, k*20, height);
+            if (mHorizontal) {
+                child.setTranslationX(y);
+            } else {
+                child.setTranslationY(y);
+            }
+            if (mHorizontal) {
+                child.setRotationY(-rot);
+            } else {
+                child.setRotationX(rot);
+            }
+        }
+    }
+
+    static class ContentLayout extends LinearLayout {
+
+        NavTabScroller mScroller;
+
+        public ContentLayout(Context context, NavTabScroller scroller) {
+            super(context);
+            mScroller = scroller;
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            if (mScroller.getGap() != 0) {
+                View v = getChildAt(0);
+                if (v != null) {
+                    if (mScroller.isHorizontal()) {
+                        int total = v.getMeasuredWidth() + getMeasuredWidth();
+                        setMeasuredDimension(total, getMeasuredHeight());
+                    } else {
+                        int total = v.getMeasuredHeight() + getMeasuredHeight();
+                        setMeasuredDimension(getMeasuredWidth(), total);
+                    }
+                }
+
+            }
+        }
+
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/NavTabView.java b/src/com/android/browser/NavTabView.java
new file mode 100644
index 0000000..3bcd7a2
--- /dev/null
+++ b/src/com/android/browser/NavTabView.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.graphics.Bitmap;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+public class NavTabView extends LinearLayout {
+
+    private ViewGroup mContent;
+    private Tab mTab;
+    private ImageView mClose;
+    private TextView mTitle;
+    private View mTitleBar;
+    ImageView mImage;
+    private OnClickListener mClickListener;
+    private boolean mHighlighted;
+
+    public NavTabView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    public NavTabView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public NavTabView(Context context) {
+        super(context);
+        init();
+    }
+
+    private void init() {
+        LayoutInflater.from(getContext()).inflate(R.layout.nav_tab_view, this);
+        mContent = (ViewGroup) findViewById(R.id.main);
+        mClose = (ImageView) findViewById(R.id.closetab);
+        mTitle = (TextView) findViewById(R.id.title);
+        mTitleBar = findViewById(R.id.titlebar);
+        mImage = (ImageView) findViewById(R.id.tab_view);
+    }
+
+    protected boolean isClose(View v) {
+        return v == mClose;
+    }
+
+    protected boolean isTitle(View v) {
+        return v == mTitleBar;
+    }
+
+    protected boolean isWebView(View v) {
+        return v == mImage;
+    }
+
+    private void setTitle() {
+        if (mTab == null) return;
+        if (mHighlighted) {
+            mTitle.setText(mTab.getUrl());
+        } else {
+            String txt = mTab.getTitle();
+            if (txt == null) {
+                txt = mTab.getUrl();
+            }
+            mTitle.setText(txt);
+        }
+        if (mTab.isSnapshot()) {
+            setTitleIcon(R.drawable.ic_history_holo_dark);
+        } else if (mTab.isPrivateBrowsingEnabled()) {
+            setTitleIcon(R.drawable.ic_incognito_holo_dark);
+        } else {
+            setTitleIcon(0);
+        }
+    }
+
+    private void setTitleIcon(int id) {
+        if (id == 0) {
+            mTitle.setPadding(mTitle.getCompoundDrawablePadding(), 0, 0, 0);
+        } else {
+            mTitle.setPadding(0, 0, 0, 0);
+        }
+        mTitle.setCompoundDrawablesWithIntrinsicBounds(id, 0, 0, 0);
+    }
+
+    protected boolean isHighlighted() {
+        return mHighlighted;
+    }
+
+    protected void setWebView(Tab tab) {
+        mTab = tab;
+        setTitle();
+        Bitmap image = tab.getScreenshot();
+        if (image != null) {
+            mImage.setImageBitmap(image);
+            if (tab != null) {
+                mImage.setContentDescription(tab.getTitle());
+            }
+        }
+    }
+
+    @Override
+    public void setOnClickListener(OnClickListener listener) {
+        mClickListener = listener;
+        mTitleBar.setOnClickListener(mClickListener);
+        mClose.setOnClickListener(mClickListener);
+        if (mImage != null) {
+            mImage.setOnClickListener(mClickListener);
+        }
+    }
+
+}
diff --git a/src/com/android/browser/NavigationBarBase.java b/src/com/android/browser/NavigationBarBase.java
new file mode 100644
index 0000000..0cf23ee
--- /dev/null
+++ b/src/com/android/browser/NavigationBarBase.java
@@ -0,0 +1,382 @@
+/*
+ * Copyright (C) 2011 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.SearchManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnFocusChangeListener;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.UrlInputView.UrlInputListener;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+
+import org.codeaurora.swe.WebView;
+
+public class NavigationBarBase extends LinearLayout implements
+        OnClickListener, UrlInputListener, OnFocusChangeListener,
+        TextWatcher {
+
+    private final static String TAG = "NavigationBarBase";
+
+    protected BaseUi mBaseUi;
+    protected TitleBar mTitleBar;
+    protected UiController mUiController;
+    protected UrlInputView mUrlInput;
+
+    private ImageView mFavicon;
+    private ImageView mLockIcon;
+
+    public NavigationBarBase(Context context) {
+        super(context);
+    }
+
+    public NavigationBarBase(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NavigationBarBase(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mLockIcon = (ImageView) findViewById(R.id.lock);
+        mFavicon = (ImageView) findViewById(R.id.favicon);
+        mUrlInput = (UrlInputView) findViewById(R.id.url);
+        mUrlInput.setUrlInputListener(this);
+        mUrlInput.setOnFocusChangeListener(this);
+        mUrlInput.setSelectAllOnFocus(true);
+        mUrlInput.addTextChangedListener(this);
+    }
+
+    public void setTitleBar(TitleBar titleBar) {
+        mTitleBar = titleBar;
+        mBaseUi = mTitleBar.getUi();
+        mUiController = mTitleBar.getUiController();
+        mUrlInput.setController(mUiController);
+    }
+
+    public void setLock(Drawable d) {
+        if (mLockIcon == null) return;
+        if (d == null) {
+            mLockIcon.setVisibility(View.GONE);
+        } else {
+            mLockIcon.setImageDrawable(d);
+            mLockIcon.setVisibility(View.VISIBLE);
+        }
+    }
+
+    public void setFavicon(Bitmap icon) {
+        if (mFavicon == null) return;
+        mFavicon.setImageDrawable(mBaseUi.getFaviconDrawable(icon));
+    }
+
+    @Override
+    public void onClick(View v) {
+    }
+
+    @Override
+    public void onFocusChange(View view, boolean hasFocus) {
+        // if losing focus and not in touch mode, leave as is
+        if (hasFocus || view.isInTouchMode() || mUrlInput.needsUpdate()) {
+            setFocusState(hasFocus);
+        }
+        if (hasFocus) {
+            mBaseUi.showTitleBar();
+        } else if (!mUrlInput.needsUpdate()) {
+            mUrlInput.dismissDropDown();
+            mUrlInput.hideIME();
+            if (mUrlInput.getText().length() == 0) {
+                Tab currentTab = mUiController.getTabControl().getCurrentTab();
+                if (currentTab != null) {
+                    setDisplayTitle(currentTab.getUrl());
+                }
+            }
+            mBaseUi.suggestHideTitleBar();
+        }
+        mUrlInput.clearNeedsUpdate();
+    }
+
+    protected void setFocusState(boolean focus) {
+    }
+
+    public boolean isEditingUrl() {
+        return mUrlInput.hasFocus();
+    }
+
+    void stopEditingUrl() {
+        WebView currentTopWebView = mUiController.getCurrentTopWebView();
+        if (currentTopWebView != null) {
+            currentTopWebView.requestFocus();
+        }
+    }
+
+    void setDisplayTitle(String title) {
+        if (!isEditingUrl()) {
+            if (!title.equals(mUrlInput.getText().toString())) {
+                mUrlInput.setText(title, false);
+            }
+        }
+    }
+
+    void setIncognitoMode(boolean incognito) {
+        mUrlInput.setIncognitoMode(incognito);
+    }
+
+    void clearCompletions() {
+        mUrlInput.dismissDropDown();
+    }
+
+ // UrlInputListener implementation
+
+    /**
+     * callback from suggestion dropdown
+     * user selected a suggestion
+     */
+    @Override
+    public void onAction(String text, String extra, String source) {
+        stopEditingUrl();
+        if (UrlInputView.TYPED.equals(source)) {
+            String url = null;
+            Object[] params  = {new String("persist.env.browser.wap2estore"),
+                                    Boolean.valueOf(false)};
+            Class[] type = new Class[] {String.class, boolean.class};
+            Boolean wap2estore = (Boolean) ReflectHelper.invokeStaticMethod(
+                      "android.os.SystemProperties", "getBoolean", type, params);
+            if ((wap2estore && isEstoreTypeUrl(text)) || isRtspTypeUrl(text)) {
+                url = text;
+            } else {
+                url = UrlUtils.smartUrlFilter(text, false);
+            }
+
+            Tab t = mBaseUi.getActiveTab();
+            // Only shortcut javascript URIs for now, as there is special
+            // logic in UrlHandler for other schemas
+            if (url != null && t != null && url.startsWith("javascript:")) {
+                mUiController.loadUrl(t, url);
+                setDisplayTitle(text);
+                return;
+            }
+
+            // add for carrier wap2estore feature
+            if (url != null && t != null && wap2estore && isEstoreTypeUrl(url)) {
+                handleEstoreTypeUrl(url);
+                setDisplayTitle(text);
+                return;
+            }
+            // add for rtsp scheme feature
+            if (url != null && t != null && isRtspTypeUrl(url)) {
+                if (handleRtspTypeUrl(url)) {
+                    return;
+                }
+            }
+        }
+        Intent i = new Intent();
+        String action = Intent.ACTION_SEARCH;
+        i.setAction(action);
+        i.putExtra(SearchManager.QUERY, text);
+        if (extra != null) {
+            i.putExtra(SearchManager.EXTRA_DATA_KEY, extra);
+        }
+        if (source != null) {
+            Bundle appData = new Bundle();
+            appData.putString("source", source);
+            i.putExtra("source", appData);
+        }
+        mUiController.handleNewIntent(i);
+        setDisplayTitle(text);
+    }
+
+    private boolean isEstoreTypeUrl(String url) {
+        String utf8Url = null;
+        try {
+            utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "err " + e);
+        }
+        if (utf8Url != null && utf8Url.startsWith("estore:")) {
+            return true;
+        }
+        return false;
+    }
+
+    private void handleEstoreTypeUrl(String url) {
+        String utf8Url = null, finalUrl = null;
+        try {
+            utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "err " + e);
+        }
+        if (utf8Url != null) {
+            finalUrl = utf8Url;
+        } else {
+            finalUrl = url;
+        }
+        if (finalUrl.replaceFirst("estore:", "").length() > 256) {
+            Toast.makeText(getContext(), R.string.estore_url_warning, Toast.LENGTH_LONG).show();
+            return;
+        }
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setData(Uri.parse(finalUrl));
+        try {
+            getContext().startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            String downloadUrl = getContext().getResources().getString(R.string.estore_homepage);
+            mUiController.loadUrl(mBaseUi.getActiveTab(), downloadUrl);
+            Toast.makeText(getContext(), R.string.download_estore_app, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    private boolean isRtspTypeUrl(String url) {
+        String utf8Url = null;
+        try {
+            utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "err " + e);
+        }
+        if (utf8Url != null && utf8Url.startsWith("rtsp://")) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean handleRtspTypeUrl(String url) {
+        Intent intent;
+        // perform generic parsing of the URI to turn it into an Intent.
+        try {
+            intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+        } catch (URISyntaxException ex) {
+            Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
+            return false;
+        }
+
+        try {
+            getContext().startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            Log.w("Browser", "No resolveActivity " + url);
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void onDismiss() {
+        final Tab currentTab = mBaseUi.getActiveTab();
+        mBaseUi.hideTitleBar();
+        post(new Runnable() {
+            public void run() {
+                clearFocus();
+                if (currentTab != null) {
+                    setDisplayTitle(currentTab.getUrl());
+                }
+            }
+        });
+    }
+
+    /**
+     * callback from the suggestion dropdown
+     * copy text to input field and stay in edit mode
+     */
+    @Override
+    public void onCopySuggestion(String text) {
+        mUrlInput.setText(text, true);
+        if (text != null) {
+            mUrlInput.setSelection(text.length());
+        }
+    }
+
+    public void setCurrentUrlIsBookmark(boolean isBookmark) {
+    }
+
+    @Override
+    public boolean dispatchKeyEventPreIme(KeyEvent evt) {
+        if (evt.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+            // catch back key in order to do slightly more cleanup than usual
+            stopEditingUrl();
+            return true;
+        }
+        return super.dispatchKeyEventPreIme(evt);
+    }
+
+    /**
+     * called from the Ui when the user wants to edit
+     * @param clearInput clear the input field
+     */
+    void startEditingUrl(boolean clearInput, boolean forceIME) {
+        // editing takes preference of progress
+        setVisibility(View.VISIBLE);
+        if (mTitleBar.useQuickControls()) {
+            mTitleBar.getProgressView().setVisibility(View.GONE);
+        }
+        if (!mUrlInput.hasFocus()) {
+            mUrlInput.requestFocus();
+        }
+        if (clearInput) {
+            mUrlInput.setText("");
+        }
+        if (forceIME) {
+            mUrlInput.showIME();
+        }
+    }
+
+    public void onProgressStarted() {
+    }
+
+    public void onProgressStopped() {
+    }
+
+    public boolean isMenuShowing() {
+        return false;
+    }
+
+    public void onTabDataChanged(Tab tab) {
+    }
+
+    public void onVoiceResult(String s) {
+        startEditingUrl(true, true);
+        onCopySuggestion(s);
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) { }
+
+    @Override
+    public void afterTextChanged(Editable s) { }
+
+}
diff --git a/src/com/android/browser/NavigationBarPhone.java b/src/com/android/browser/NavigationBarPhone.java
new file mode 100644
index 0000000..50ddea2
--- /dev/null
+++ b/src/com/android/browser/NavigationBarPhone.java
@@ -0,0 +1,278 @@
+/*
+ * Copyright (C) 2011 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.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewConfiguration;
+import org.codeaurora.swe.WebView;
+import android.widget.ImageView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnDismissListener;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+
+import com.android.browser.R;
+import com.android.browser.UrlInputView.StateListener;
+
+public class NavigationBarPhone extends NavigationBarBase implements
+        StateListener, OnMenuItemClickListener, OnDismissListener {
+
+    private ImageView mStopButton;
+    private ImageView mMagnify;
+    private ImageView mClearButton;
+    private ImageView mVoiceButton;
+    private Drawable mStopDrawable;
+    private Drawable mRefreshDrawable;
+    private String mStopDescription;
+    private String mRefreshDescription;
+    private View mTabSwitcher;
+    private View mComboIcon;
+    private View mTitleContainer;
+    private View mMore;
+    private Drawable mTextfieldBgDrawable;
+    private PopupMenu mPopupMenu;
+    private boolean mOverflowMenuShowing;
+    private boolean mNeedsMenu;
+    private View mIncognitoIcon;
+
+    public NavigationBarPhone(Context context) {
+        super(context);
+    }
+
+    public NavigationBarPhone(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public NavigationBarPhone(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mStopButton = (ImageView) findViewById(R.id.stop);
+        mStopButton.setOnClickListener(this);
+        mClearButton = (ImageView) findViewById(R.id.clear);
+        mClearButton.setOnClickListener(this);
+        mVoiceButton = (ImageView) findViewById(R.id.voice);
+        mVoiceButton.setOnClickListener(this);
+        mMagnify = (ImageView) findViewById(R.id.magnify);
+        mTabSwitcher = findViewById(R.id.tab_switcher);
+        mTabSwitcher.setOnClickListener(this);
+        mMore = findViewById(R.id.more);
+        mMore.setOnClickListener(this);
+        mComboIcon = findViewById(R.id.iconcombo);
+        mComboIcon.setOnClickListener(this);
+        mTitleContainer = findViewById(R.id.title_bg);
+        setFocusState(false);
+        Resources res = getContext().getResources();
+        mStopDrawable = res.getDrawable(R.drawable.ic_stop_holo_dark);
+        mRefreshDrawable = res.getDrawable(R.drawable.ic_refresh_holo_dark);
+        mStopDescription = res.getString(R.string.accessibility_button_stop);
+        mRefreshDescription = res.getString(R.string.accessibility_button_refresh);
+        mTextfieldBgDrawable = res.getDrawable(R.drawable.textfield_active_holo_dark);
+        mUrlInput.setContainer(this);
+        mUrlInput.setStateListener(this);
+        mNeedsMenu = !ViewConfiguration.get(getContext()).hasPermanentMenuKey();
+        mIncognitoIcon = findViewById(R.id.incognito_icon);
+    }
+
+    @Override
+    public void onProgressStarted() {
+        super.onProgressStarted();
+        if (mStopButton.getDrawable() != mStopDrawable) {
+            mStopButton.setImageDrawable(mStopDrawable);
+            mStopButton.setContentDescription(mStopDescription);
+            if (mStopButton.getVisibility() != View.VISIBLE) {
+                mComboIcon.setVisibility(View.GONE);
+                mStopButton.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    @Override
+    public void onProgressStopped() {
+        super.onProgressStopped();
+        mStopButton.setImageDrawable(mRefreshDrawable);
+        mStopButton.setContentDescription(mRefreshDescription);
+        if (!isEditingUrl()) {
+            mComboIcon.setVisibility(View.VISIBLE);
+        }
+        onStateChanged(mUrlInput.getState());
+    }
+
+    /**
+     * Update the text displayed in the title bar.
+     * @param title String to display.  If null, the new tab string will be
+     *      shown.
+     */
+    @Override
+    void setDisplayTitle(String title) {
+        mUrlInput.setTag(title);
+        if (!isEditingUrl()) {
+            if (title == null) {
+                mUrlInput.setText(R.string.new_tab);
+            } else {
+                Tab tab = mUiController.getTabControl().getCurrentTab();
+                if (tab != null && tab.getUrl() != null &&
+                        tab.getUrl().startsWith("http://218.206.177.209:8080/waptest/browser15")) {
+                    //for cmcc test case, display website title for specified cmcc website,
+                    //not url address.
+                    mUrlInput.setText(tab.getTitle(), false);
+                } else {
+                    mUrlInput.setText(UrlUtils.stripUrl(title), false);
+                }
+            }
+            mUrlInput.setSelection(0);
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (v == mStopButton) {
+            if (mTitleBar.isInLoad()) {
+                mUiController.stopLoading();
+            } else {
+                WebView web = mBaseUi.getWebView();
+                if (web != null) {
+                    stopEditingUrl();
+                    Tab currentTab = mUiController.getTabControl().getCurrentTab();
+                    if (currentTab.hasCrashed) {
+                        currentTab.replaceCrashView(web, currentTab.getViewContainer());
+                    }
+                    web.reload();
+                }
+            }
+        } else if (v == mTabSwitcher) {
+            ((PhoneUi) mBaseUi).toggleNavScreen();
+        } else if (mMore == v) {
+            showMenu(mMore);
+        } else if (mClearButton == v) {
+            mUrlInput.setText("");
+        } else if (mComboIcon == v) {
+            mUiController.showPageInfo();
+        } else if (mVoiceButton == v) {
+            mUiController.startVoiceRecognizer();
+        } else {
+            super.onClick(v);
+        }
+    }
+
+    @Override
+    public boolean isMenuShowing() {
+        return super.isMenuShowing() || mOverflowMenuShowing;
+    }
+
+    void showMenu(View anchor) {
+        Activity activity = mUiController.getActivity();
+        if (mPopupMenu == null) {
+            mPopupMenu = new PopupMenu(getContext(), anchor);
+            mPopupMenu.setOnMenuItemClickListener(this);
+            mPopupMenu.setOnDismissListener(this);
+            if (!activity.onCreateOptionsMenu(mPopupMenu.getMenu())) {
+                mPopupMenu = null;
+                return;
+            }
+        }
+        Menu menu = mPopupMenu.getMenu();
+        if (activity.onPrepareOptionsMenu(menu)) {
+            mOverflowMenuShowing = true;
+            mPopupMenu.show();
+        }
+    }
+
+    @Override
+    public void onDismiss(PopupMenu menu) {
+        if (menu == mPopupMenu) {
+            onMenuHidden();
+        }
+    }
+
+    private void onMenuHidden() {
+        mOverflowMenuShowing = false;
+        mBaseUi.showTitleBarForDuration();
+    }
+
+    @Override
+    public void onFocusChange(View view, boolean hasFocus) {
+        if (view == mUrlInput) {
+            if (hasFocus && !mUrlInput.getText().toString().equals(mUrlInput.getTag())) {
+                // only change text if different
+                mUrlInput.setText((String) mUrlInput.getTag(), false);
+                mUrlInput.selectAll();
+            } else {
+                setDisplayTitle(mUrlInput.getText().toString());
+            }
+        }
+        super.onFocusChange(view, hasFocus);
+    }
+
+    @Override
+    public void onStateChanged(int state) {
+        mVoiceButton.setVisibility(View.GONE);
+        switch(state) {
+        case StateListener.STATE_NORMAL:
+            mComboIcon.setVisibility(View.VISIBLE);
+            mStopButton.setVisibility(View.GONE);
+            mClearButton.setVisibility(View.GONE);
+            mMagnify.setVisibility(View.GONE);
+            mTabSwitcher.setVisibility(View.VISIBLE);
+            mTitleContainer.setBackgroundDrawable(null);
+            mMore.setVisibility(mNeedsMenu ? View.VISIBLE : View.GONE);
+            break;
+        case StateListener.STATE_HIGHLIGHTED:
+            mComboIcon.setVisibility(View.GONE);
+            mStopButton.setVisibility(View.VISIBLE);
+            mClearButton.setVisibility(View.GONE);
+            if ((mUiController != null) && mUiController.supportsVoice()) {
+                mVoiceButton.setVisibility(View.VISIBLE);
+            }
+            mMagnify.setVisibility(View.GONE);
+            mTabSwitcher.setVisibility(View.GONE);
+            mMore.setVisibility(View.GONE);
+            mTitleContainer.setBackgroundDrawable(mTextfieldBgDrawable);
+            break;
+        case StateListener.STATE_EDITED:
+            mComboIcon.setVisibility(View.GONE);
+            mStopButton.setVisibility(View.GONE);
+            mClearButton.setVisibility(View.VISIBLE);
+            mMagnify.setVisibility(View.VISIBLE);
+            mTabSwitcher.setVisibility(View.GONE);
+            mMore.setVisibility(View.GONE);
+            mTitleContainer.setBackgroundDrawable(mTextfieldBgDrawable);
+            break;
+        }
+    }
+
+    @Override
+    public void onTabDataChanged(Tab tab) {
+        super.onTabDataChanged(tab);
+        mIncognitoIcon.setVisibility(tab.isPrivateBrowsingEnabled()
+                ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        return mUiController.onOptionsItemSelected(item);
+    }
+
+}
diff --git a/src/com/android/browser/NavigationBarTablet.java b/src/com/android/browser/NavigationBarTablet.java
new file mode 100644
index 0000000..ebe40ea
--- /dev/null
+++ b/src/com/android/browser/NavigationBarTablet.java
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2011 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.Drawable;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.UrlInputView.StateListener;
+
+public class NavigationBarTablet extends NavigationBarBase implements StateListener {
+
+    private Drawable mStopDrawable;
+    private Drawable mReloadDrawable;
+    private String mStopDescription;
+    private String mRefreshDescription;
+
+    private View mUrlContainer;
+    private ImageButton mBackButton;
+    private ImageButton mForwardButton;
+    private ImageView mStar;
+    private ImageView mUrlIcon;
+    private ImageView mSearchButton;
+    private ImageView mStopButton;
+    private View mAllButton;
+    private View mClearButton;
+    private View mVoiceButton;
+    private View mNavButtons;
+    private Drawable mFocusDrawable;
+    private Drawable mUnfocusDrawable;
+    private boolean mHideNavButtons;
+    private Drawable mFaviconDrawable;
+
+    public NavigationBarTablet(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public NavigationBarTablet(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public NavigationBarTablet(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    private void init(Context context) {
+        Resources resources = context.getResources();
+        mStopDrawable = resources.getDrawable(R.drawable.ic_stop_holo_dark);
+        mReloadDrawable = resources.getDrawable(R.drawable.ic_refresh_holo_dark);
+        mStopDescription = resources.getString(R.string.accessibility_button_stop);
+        mRefreshDescription = resources.getString(R.string.accessibility_button_refresh);
+        mFocusDrawable = resources.getDrawable(
+                R.drawable.textfield_active_holo_dark);
+        mUnfocusDrawable = resources.getDrawable(
+                R.drawable.textfield_default_holo_dark);
+        mHideNavButtons = resources.getBoolean(R.bool.hide_nav_buttons);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mAllButton = findViewById(R.id.all_btn);
+        // TODO: Change enabled states based on whether you can go
+        // back/forward.  Probably should be done inside onPageStarted.
+        mNavButtons = findViewById(R.id.navbuttons);
+        mBackButton = (ImageButton) findViewById(R.id.back);
+        mForwardButton = (ImageButton) findViewById(R.id.forward);
+        mUrlIcon = (ImageView) findViewById(R.id.url_icon);
+        mStar = (ImageView) findViewById(R.id.star);
+        mStopButton = (ImageView) findViewById(R.id.stop);
+        mSearchButton = (ImageView) findViewById(R.id.search);
+        mClearButton = findViewById(R.id.clear);
+        mVoiceButton = findViewById(R.id.voice);
+        mUrlContainer = findViewById(R.id.urlbar_focused);
+        mBackButton.setOnClickListener(this);
+        mForwardButton.setOnClickListener(this);
+        mStar.setOnClickListener(this);
+        mAllButton.setOnClickListener(this);
+        mStopButton.setOnClickListener(this);
+        mSearchButton.setOnClickListener(this);
+        mClearButton.setOnClickListener(this);
+        mVoiceButton.setOnClickListener(this);
+        mUrlInput.setContainer(mUrlContainer);
+        mUrlInput.setStateListener(this);
+    }
+
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        Resources res = getContext().getResources();
+        mHideNavButtons = res.getBoolean(R.bool.hide_nav_buttons);
+        if (mUrlInput.hasFocus()) {
+            if (mHideNavButtons && (mNavButtons.getVisibility() == View.VISIBLE)) {
+                int aw = mNavButtons.getMeasuredWidth();
+                mNavButtons.setVisibility(View.GONE);
+                mNavButtons.setAlpha(0f);
+                mNavButtons.setTranslationX(-aw);
+            } else if (!mHideNavButtons && (mNavButtons.getVisibility() == View.GONE)) {
+                mNavButtons.setVisibility(View.VISIBLE);
+                mNavButtons.setAlpha(1f);
+                mNavButtons.setTranslationX(0);
+            }
+        }
+    }
+
+    @Override
+    public void setTitleBar(TitleBar titleBar) {
+        super.setTitleBar(titleBar);
+        setFocusState(false);
+    }
+
+    void updateNavigationState(Tab tab) {
+        if (tab != null) {
+            mBackButton.setImageResource(tab.canGoBack()
+                    ? R.drawable.ic_back_holo_dark
+                    : R.drawable.ic_back_disabled_holo_dark);
+            mForwardButton.setImageResource(tab.canGoForward()
+                    ? R.drawable.ic_forward_holo_dark
+                    : R.drawable.ic_forward_disabled_holo_dark);
+        }
+        updateUrlIcon();
+    }
+
+    @Override
+    public void onTabDataChanged(Tab tab) {
+        super.onTabDataChanged(tab);
+        showHideStar(tab);
+    }
+
+    @Override
+    public void setCurrentUrlIsBookmark(boolean isBookmark) {
+        mStar.setActivated(isBookmark);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if ((mBackButton == v) && (mUiController.getCurrentTab() != null)) {
+            mUiController.getCurrentTab().goBack();
+        } else if ((mForwardButton == v)  && (mUiController.getCurrentTab() != null)) {
+            mUiController.getCurrentTab().goForward();
+        } else if (mStar == v) {
+            Intent intent = mUiController.createBookmarkCurrentPageIntent(true);
+            if (intent != null) {
+                getContext().startActivity(intent);
+            }
+        } else if (mAllButton == v) {
+            mUiController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+        } else if (mSearchButton == v) {
+            mBaseUi.editUrl(true, true);
+        } else if (mStopButton == v) {
+            stopOrRefresh();
+        } else if (mClearButton == v) {
+            clearOrClose();
+        } else if (mVoiceButton == v) {
+            mUiController.startVoiceRecognizer();
+        } else {
+            super.onClick(v);
+        }
+    }
+
+    private void clearOrClose() {
+        if (TextUtils.isEmpty(mUrlInput.getText())) {
+            // close
+            mUrlInput.clearFocus();
+        } else {
+            // clear
+            mUrlInput.setText("");
+        }
+    }
+
+    @Override
+    public void setFavicon(Bitmap icon) {
+        mFaviconDrawable = mBaseUi.getFaviconDrawable(icon);
+        updateUrlIcon();
+    }
+
+    void updateUrlIcon() {
+        if (mUrlInput.hasFocus()) {
+            mUrlIcon.setImageResource(R.drawable.ic_search_holo_dark);
+        } else {
+            if (mFaviconDrawable == null) {
+                mFaviconDrawable = mBaseUi.getFaviconDrawable(null);
+            }
+            mUrlIcon.setImageDrawable(mFaviconDrawable);
+        }
+    }
+
+    @Override
+    protected void setFocusState(boolean focus) {
+        super.setFocusState(focus);
+        if (focus) {
+            if (mHideNavButtons) {
+                hideNavButtons();
+            }
+            mSearchButton.setVisibility(View.GONE);
+            mStar.setVisibility(View.GONE);
+            mUrlIcon.setImageResource(R.drawable.ic_search_holo_dark);
+        } else {
+            if (mHideNavButtons) {
+                showNavButtons();
+            }
+            showHideStar(mUiController.getCurrentTab());
+            if (mTitleBar.useQuickControls()) {
+                mSearchButton.setVisibility(View.GONE);
+            } else {
+                mSearchButton.setVisibility(View.VISIBLE);
+            }
+            updateUrlIcon();
+        }
+        mUrlContainer.setBackgroundDrawable(focus
+                ? mFocusDrawable : mUnfocusDrawable);
+    }
+
+    private void stopOrRefresh() {
+        if (mUiController == null) return;
+        if (mTitleBar.isInLoad()) {
+            mUiController.stopLoading();
+        } else {
+            if (mUiController.getCurrentTopWebView() != null) {
+                Tab currTab = mUiController.getTabControl().getCurrentTab();
+                if (currTab.hasCrashed) {
+                    currTab.replaceCrashView(mUiController.getCurrentTopWebView(),
+                        currTab.getViewContainer());
+                }
+                mUiController.getCurrentTopWebView().reload();
+            }
+        }
+    }
+
+    @Override
+    public void onProgressStarted() {
+        mStopButton.setImageDrawable(mStopDrawable);
+        mStopButton.setContentDescription(mStopDescription);
+    }
+
+    @Override
+    public void onProgressStopped() {
+        mStopButton.setImageDrawable(mReloadDrawable);
+        mStopButton.setContentDescription(mRefreshDescription);
+    }
+
+    private AnimatorSet mAnimation;
+
+    private void hideNavButtons() {
+        if (mBaseUi.blockFocusAnimations()) {
+            mNavButtons.setVisibility(View.GONE);
+            return;
+        }
+        int awidth = mNavButtons.getMeasuredWidth();
+        Animator anim1 = ObjectAnimator.ofFloat(mNavButtons, View.TRANSLATION_X, 0, - awidth);
+        Animator anim2 = ObjectAnimator.ofInt(mUrlContainer, "left", mUrlContainer.getLeft(),
+                mUrlContainer.getPaddingLeft());
+        Animator anim3 = ObjectAnimator.ofFloat(mNavButtons, View.ALPHA, 1f, 0f);
+        mAnimation = new AnimatorSet();
+        mAnimation.playTogether(anim1, anim2, anim3);
+        mAnimation.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mNavButtons.setVisibility(View.GONE);
+                mAnimation = null;
+            }
+        });
+        mAnimation.setDuration(150);
+        mAnimation.start();
+    }
+
+    private void showNavButtons() {
+        if (mAnimation != null) {
+            mAnimation.cancel();
+        }
+        mNavButtons.setVisibility(View.VISIBLE);
+        mNavButtons.setTranslationX(0);
+        if (!mBaseUi.blockFocusAnimations()) {
+            int awidth = mNavButtons.getMeasuredWidth();
+            Animator anim1 = ObjectAnimator.ofFloat(mNavButtons,
+                    View.TRANSLATION_X, -awidth, 0);
+            Animator anim2 = ObjectAnimator.ofInt(mUrlContainer, "left", 0,
+                    awidth);
+            Animator anim3 = ObjectAnimator.ofFloat(mNavButtons, View.ALPHA,
+                    0f, 1f);
+            AnimatorSet combo = new AnimatorSet();
+            combo.playTogether(anim1, anim2, anim3);
+            combo.setDuration(150);
+            combo.start();
+        }
+    }
+
+    private void showHideStar(Tab tab) {
+        // hide the bookmark star for data URLs
+        if (tab != null && tab.inForeground()) {
+            int starVisibility = View.VISIBLE;
+            String url = tab.getUrl();
+            if (DataUri.isDataUri(url)) {
+                starVisibility = View.GONE;
+            }
+            mStar.setVisibility(starVisibility);
+        }
+    }
+
+    @Override
+    public void onStateChanged(int state) {
+        mVoiceButton.setVisibility(View.GONE);
+        switch(state) {
+        case STATE_NORMAL:
+            mClearButton.setVisibility(View.GONE);
+            break;
+        case STATE_HIGHLIGHTED:
+            mClearButton.setVisibility(View.GONE);
+            if ((mUiController != null) && mUiController.supportsVoice()) {
+                mVoiceButton.setVisibility(View.VISIBLE);
+            }
+            break;
+        case STATE_EDITED:
+            mClearButton.setVisibility(View.VISIBLE);
+            break;
+        }
+    }
+
+}
diff --git a/src/com/android/browser/NetworkStateHandler.java b/src/com/android/browser/NetworkStateHandler.java
new file mode 100644
index 0000000..74a355a
--- /dev/null
+++ b/src/com/android/browser/NetworkStateHandler.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserSettings;
+
+/**
+ * Handle network state changes
+ */
+public class NetworkStateHandler {
+
+    Activity mActivity;
+    Controller mController;
+
+    // monitor platform changes
+    private IntentFilter mNetworkStateChangedFilter;
+    private BroadcastReceiver mNetworkStateIntentReceiver;
+    private boolean mIsNetworkUp;
+
+    public NetworkStateHandler(Activity activity, Controller controller) {
+        mActivity = activity;
+        mController = controller;
+        // Find out if the network is currently up.
+        ConnectivityManager cm = (ConnectivityManager) mActivity
+                .getSystemService(Context.CONNECTIVITY_SERVICE);
+        NetworkInfo info = cm.getActiveNetworkInfo();
+        if (info != null) {
+            mIsNetworkUp = info.isAvailable();
+        }
+
+        /*
+         * enables registration for changes in network status from http stack
+         */
+        mNetworkStateChangedFilter = new IntentFilter();
+        mNetworkStateChangedFilter.addAction(
+                ConnectivityManager.CONNECTIVITY_ACTION);
+        mNetworkStateIntentReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                if (intent.getAction().equals(
+                        ConnectivityManager.CONNECTIVITY_ACTION)) {
+
+                    NetworkInfo info = intent.getParcelableExtra(
+                            ConnectivityManager.EXTRA_NETWORK_INFO);
+                    String typeName = info.getTypeName();
+                    String subtypeName = info.getSubtypeName();
+                    sendNetworkType(typeName.toLowerCase(),
+                            (subtypeName != null ? subtypeName.toLowerCase() : ""));
+                    BrowserSettings.getInstance().updateConnectionType();
+
+                    boolean noConnection = intent.getBooleanExtra(
+                            ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+
+                    onNetworkToggle(!noConnection);
+                }
+            }
+        };
+
+    }
+
+    void onPause() {
+        // unregister network state listener
+        mActivity.unregisterReceiver(mNetworkStateIntentReceiver);
+    }
+
+    void onResume() {
+        mActivity.registerReceiver(mNetworkStateIntentReceiver,
+                mNetworkStateChangedFilter);
+        BrowserSettings.getInstance().updateConnectionType();
+    }
+
+    /**
+     * connectivity manager says net has come or gone... inform the user
+     * @param up true if net has come up, false if net has gone down
+     */
+    void onNetworkToggle(boolean up) {
+        if (up == mIsNetworkUp) {
+            return;
+        }
+        mIsNetworkUp = up;
+        WebView w = mController.getCurrentWebView();
+        if (w != null) {
+            w.setNetworkAvailable(up);
+        }
+    }
+
+    boolean isNetworkUp() {
+        return mIsNetworkUp;
+    }
+
+    private void sendNetworkType(String type, String subtype) {
+        WebView w = mController.getCurrentWebView();
+        if (w != null ) {
+            w.setNetworkType(type, subtype);
+        }
+    }
+}
diff --git a/src/com/android/browser/NfcHandler.java b/src/com/android/browser/NfcHandler.java
new file mode 100644
index 0000000..0dd8576
--- /dev/null
+++ b/src/com/android/browser/NfcHandler.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2011 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.nfc.NdefMessage;
+import android.nfc.NdefRecord;
+import android.nfc.NfcAdapter;
+import android.nfc.NfcEvent;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+import java.util.concurrent.CountDownLatch;
+
+/** This class implements sharing the URL of the currently
+  * shown browser page over NFC. Sharing is only active
+  * when the activity is in the foreground and resumed.
+  * Incognito tabs will not be shared over NFC.
+  */
+public class NfcHandler implements NfcAdapter.CreateNdefMessageCallback {
+    static final String TAG = "BrowserNfcHandler";
+    static final int GET_PRIVATE_BROWSING_STATE_MSG = 100;
+
+    final Controller mController;
+
+    Tab mCurrentTab;
+    boolean mIsPrivate;
+    CountDownLatch mPrivateBrowsingSignal;
+
+    public static void register(Activity activity, Controller controller) {
+        NfcAdapter adapter = NfcAdapter.getDefaultAdapter(activity.getApplicationContext());
+        if (adapter == null) {
+            return;  // NFC not available on this device
+        }
+        NfcHandler handler = null;
+        if (controller != null) {
+            handler = new NfcHandler(controller);
+        }
+
+        adapter.setNdefPushMessageCallback(handler, activity);
+    }
+
+    public static void unregister(Activity activity) {
+        // Passing a null controller causes us to disable
+        // the callback and release the ref to out activity.
+        register(activity, null);
+    }
+
+    public NfcHandler(Controller controller) {
+        mController = controller;
+    }
+
+    final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == GET_PRIVATE_BROWSING_STATE_MSG) {
+                mIsPrivate = mCurrentTab.getWebView().isPrivateBrowsingEnabled();
+                mPrivateBrowsingSignal.countDown();
+            }
+        }
+    };
+
+    @Override
+    public NdefMessage createNdefMessage(NfcEvent event) {
+        mCurrentTab = mController.getCurrentTab();
+        if ((mCurrentTab != null) && (mCurrentTab.getWebView() != null)) {
+            // We can only read the WebView state on the UI thread, so post
+            // a message and wait.
+            mPrivateBrowsingSignal = new CountDownLatch(1);
+            mHandler.sendMessage(mHandler.obtainMessage(GET_PRIVATE_BROWSING_STATE_MSG));
+            try {
+                mPrivateBrowsingSignal.await();
+            } catch (InterruptedException e) {
+                return null;
+            }
+        }
+
+        if ((mCurrentTab == null) || mIsPrivate) {
+            return null;
+        }
+
+        String currentUrl = mCurrentTab.getUrl();
+        if (currentUrl != null) {
+            try {
+                return new NdefMessage(NdefRecord.createUri(currentUrl));
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "IllegalArgumentException creating URI NdefRecord", e);
+                return null;
+            }
+        } else {
+            return null;
+        }
+    }
+}
diff --git a/src/com/android/browser/OpenDownloadReceiver.java b/src/com/android/browser/OpenDownloadReceiver.java
new file mode 100644
index 0000000..4277ff4
--- /dev/null
+++ b/src/com/android/browser/OpenDownloadReceiver.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 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.DownloadManager;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+
+/**
+ * This {@link BroadcastReceiver} handles clicks to notifications that
+ * downloads from the browser are in progress/complete.  Clicking on an
+ * in-progress or failed download will open the download manager.  Clicking on
+ * a complete, successful download will open the file.
+ */
+public class OpenDownloadReceiver extends BroadcastReceiver {
+    private static Handler sAsyncHandler;
+    static {
+        HandlerThread thr = new HandlerThread("Open browser download async");
+        thr.start();
+        sAsyncHandler = new Handler(thr.getLooper());
+    }
+    @Override
+    public void onReceive(final Context context, Intent intent) {
+        String action = intent.getAction();
+        if (!DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(action)) {
+            openDownloadsPage(context);
+            return;
+        }
+        long ids[] = intent.getLongArrayExtra(
+                DownloadManager.EXTRA_NOTIFICATION_CLICK_DOWNLOAD_IDS);
+        if (ids == null || ids.length == 0) {
+            openDownloadsPage(context);
+            return;
+        }
+        final long id = ids[0];
+        final PendingResult result = goAsync();
+        Runnable worker = new Runnable() {
+            @Override
+            public void run() {
+                onReceiveAsync(context, id);
+                result.finish();
+            }
+        };
+        sAsyncHandler.post(worker);
+    }
+
+    private void onReceiveAsync(Context context, long id) {
+        DownloadManager manager = (DownloadManager) context.getSystemService(
+                Context.DOWNLOAD_SERVICE);
+        Uri uri = manager.getUriForDownloadedFile(id);
+        if (uri == null) {
+            // Open the downloads page
+            openDownloadsPage(context);
+        } else {
+            Intent launchIntent = new Intent(Intent.ACTION_VIEW);
+            launchIntent.setDataAndType(uri, manager.getMimeTypeForDownloadedFile(id));
+            launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            try {
+                context.startActivity(launchIntent);
+            } catch (ActivityNotFoundException e) {
+                openDownloadsPage(context);
+            }
+        }
+    }
+
+    /**
+     * Open the Activity which shows a list of all downloads.
+     * @param context
+     */
+    private void openDownloadsPage(Context context) {
+        Intent pageView = new Intent(DownloadManager.ACTION_VIEW_DOWNLOADS);
+        pageView.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        context.startActivity(pageView);
+    }
+}
diff --git a/src/com/android/browser/OptionsMenuHandler.java b/src/com/android/browser/OptionsMenuHandler.java
new file mode 100644
index 0000000..d602c7d
--- /dev/null
+++ b/src/com/android/browser/OptionsMenuHandler.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2011 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.view.Menu;
+import android.view.MenuItem;
+
+public interface OptionsMenuHandler {
+
+    boolean onCreateOptionsMenu(Menu menu);
+    boolean onPrepareOptionsMenu(Menu menu);
+    boolean onOptionsItemSelected(MenuItem item);
+}
diff --git a/src/com/android/browser/PageDialogsHandler.java b/src/com/android/browser/PageDialogsHandler.java
new file mode 100644
index 0000000..a38b904
--- /dev/null
+++ b/src/com/android/browser/PageDialogsHandler.java
@@ -0,0 +1,428 @@
+/*
+ * Copyright (C) 2010 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 java.lang.reflect.Method;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.net.http.SslCertificate;
+import android.net.http.SslError;
+import android.view.LayoutInflater;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+/**
+ * Displays page info
+ *
+ */
+public class PageDialogsHandler {
+
+    private Context mContext;
+    private Controller mController;
+    private boolean mPageInfoFromShowSSLCertificateOnError;
+    private String mUrlCertificateOnError;
+    private Tab mPageInfoView;
+    private AlertDialog mPageInfoDialog;
+
+    // as SSLCertificateOnError has different style for landscape / portrait,
+    // we have to re-open it when configuration changed
+    private AlertDialog mSSLCertificateOnErrorDialog;
+    private WebView mSSLCertificateOnErrorView;
+    private SslErrorHandler mSSLCertificateOnErrorHandler;
+    private SslError mSSLCertificateOnErrorError;
+
+    // as SSLCertificate has different style for landscape / portrait, we
+    // have to re-open it when configuration changed
+    private AlertDialog mSSLCertificateDialog;
+    private Tab mSSLCertificateView;
+    private HttpAuthenticationDialog mHttpAuthenticationDialog;
+
+    public PageDialogsHandler(Context context, Controller controller) {
+        mContext = context;
+        mController = controller;
+    }
+
+    public void onConfigurationChanged(Configuration config) {
+        if (mPageInfoDialog != null) {
+            mPageInfoDialog.dismiss();
+            showPageInfo(mPageInfoView,
+                         mPageInfoFromShowSSLCertificateOnError,
+                         mUrlCertificateOnError);
+        }
+        if (mSSLCertificateDialog != null) {
+            mSSLCertificateDialog.dismiss();
+            showSSLCertificate(mSSLCertificateView);
+        }
+        if (mSSLCertificateOnErrorDialog != null) {
+            mSSLCertificateOnErrorDialog.dismiss();
+            showSSLCertificateOnError(mSSLCertificateOnErrorView,
+                                      mSSLCertificateOnErrorHandler,
+                                      mSSLCertificateOnErrorError);
+        }
+        if (mHttpAuthenticationDialog != null) {
+            mHttpAuthenticationDialog.reshow();
+        }
+    }
+
+    /**
+     * Displays an http-authentication dialog.
+     */
+    void showHttpAuthentication(final Tab tab, final HttpAuthHandler handler, String host, String realm) {
+        mHttpAuthenticationDialog = new HttpAuthenticationDialog(mContext, host, realm);
+        mHttpAuthenticationDialog.setOkListener(new HttpAuthenticationDialog.OkListener() {
+            public void onOk(String host, String realm, String username, String password) {
+                setHttpAuthUsernamePassword(host, realm, username, password);
+                handler.proceed(username, password);
+                mHttpAuthenticationDialog = null;
+            }
+        });
+        mHttpAuthenticationDialog.setCancelListener(new HttpAuthenticationDialog.CancelListener() {
+            public void onCancel() {
+                handler.cancel();
+                mController.onUpdatedSecurityState(tab);
+                mHttpAuthenticationDialog = null;
+            }
+        });
+        mHttpAuthenticationDialog.show();
+    }
+
+    /**
+     * Set HTTP authentication password.
+     *
+     * @param host The host for the password
+     * @param realm The realm for the password
+     * @param username The username for the password. If it is null, it means
+     *            password can't be saved.
+     * @param password The password
+     */
+    public void setHttpAuthUsernamePassword(String host, String realm,
+                                            String username,
+                                            String password) {
+        WebView w = mController.getCurrentTopWebView();
+        if (w != null) {
+            w.setHttpAuthUsernamePassword(host, realm, username, password);
+        }
+    }
+
+    /**
+     * Displays a page-info dialog.
+     * @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.
+     * @param urlCertificateOnError The URL that invokes SSLCertificateError.
+     * Null when fromShowSSLCertificateOnError is false.
+     */
+    void showPageInfo(final Tab tab,
+            final boolean fromShowSSLCertificateOnError,
+            final String urlCertificateOnError) {
+        if (tab == null) return;
+        final LayoutInflater factory = LayoutInflater.from(mContext);
+
+        final View pageInfoView = factory.inflate(R.layout.page_info, null);
+
+        final WebView view = tab.getWebView();
+
+        String url = fromShowSSLCertificateOnError ? urlCertificateOnError : tab.getUrl();
+        String title = tab.getTitle();
+
+        if (url == null) {
+            url = "";
+        }
+        if (title == null) {
+            title = "";
+        }
+
+        ((TextView) pageInfoView.findViewById(R.id.address)).setText(url);
+        ((TextView) pageInfoView.findViewById(R.id.title)).setText(title);
+
+        mPageInfoView = tab;
+        mPageInfoFromShowSSLCertificateOnError = fromShowSSLCertificateOnError;
+        mUrlCertificateOnError = urlCertificateOnError;
+
+        AlertDialog.Builder alertDialogBuilder =
+            new AlertDialog.Builder(mContext)
+            .setTitle(R.string.page_info)
+            .setIcon(android.R.drawable.ic_dialog_info)
+            .setView(pageInfoView)
+            .setPositiveButton(
+                R.string.ok,
+                new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog,
+                                        int whichButton) {
+                        mPageInfoDialog = null;
+                        mPageInfoView = null;
+
+                        // if we came here from the SSL error dialog
+                        if (fromShowSSLCertificateOnError) {
+                            // go back to the SSL error dialog
+                            showSSLCertificateOnError(
+                                mSSLCertificateOnErrorView,
+                                mSSLCertificateOnErrorHandler,
+                                mSSLCertificateOnErrorError);
+                        }
+                    }
+                })
+            .setOnCancelListener(
+                new DialogInterface.OnCancelListener() {
+                    public void onCancel(DialogInterface dialog) {
+                        mPageInfoDialog = null;
+                        mPageInfoView = null;
+
+                        // if we came here from the SSL error dialog
+                        if (fromShowSSLCertificateOnError) {
+                            // go back to the SSL error dialog
+                            showSSLCertificateOnError(
+                                mSSLCertificateOnErrorView,
+                                mSSLCertificateOnErrorHandler,
+                                mSSLCertificateOnErrorError);
+                        }
+                    }
+                });
+
+        // if we have a main top-level page SSL certificate set or a certificate
+        // error
+        if (fromShowSSLCertificateOnError ||
+                (view != null && view.getCertificate() != null)) {
+            // add a 'View Certificate' button
+            alertDialogBuilder.setNeutralButton(
+                R.string.view_certificate,
+                new DialogInterface.OnClickListener() {
+                    public void onClick(DialogInterface dialog,
+                                        int whichButton) {
+                        mPageInfoDialog = null;
+                        mPageInfoView = null;
+
+                        // if we came here from the SSL error dialog
+                        if (fromShowSSLCertificateOnError) {
+                            // go back to the SSL error dialog
+                            showSSLCertificateOnError(
+                                mSSLCertificateOnErrorView,
+                                mSSLCertificateOnErrorHandler,
+                                mSSLCertificateOnErrorError);
+                        } else {
+                            // otherwise, display the top-most certificate from
+                            // the chain
+                            showSSLCertificate(tab);
+                        }
+                    }
+                });
+        }
+
+        mPageInfoDialog = alertDialogBuilder.show();
+    }
+
+    /**
+     * Displays the main top-level page SSL certificate dialog
+     * (accessible from the Page-Info dialog).
+     * @param tab The tab to show certificate for.
+     */
+    private void showSSLCertificate(final Tab tab) {
+
+        SslCertificate cert = tab.getWebView().getCertificate();
+        if (cert == null) {
+            return;
+        }
+
+        mSSLCertificateView = tab;
+        mSSLCertificateDialog = createSslCertificateDialog(cert, tab.getSslCertificateError())
+                .setPositiveButton(R.string.ok,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog,
+                                    int whichButton) {
+                                mSSLCertificateDialog = null;
+                                mSSLCertificateView = null;
+
+                                showPageInfo(tab, false, null);
+                            }
+                        })
+                .setOnCancelListener(
+                        new DialogInterface.OnCancelListener() {
+                            public void onCancel(DialogInterface dialog) {
+                                mSSLCertificateDialog = null;
+                                mSSLCertificateView = null;
+
+                                showPageInfo(tab, false, null);
+                            }
+                        })
+                .show();
+    }
+
+    /**
+     * Displays the SSL error certificate dialog.
+     * @param view The target web-view.
+     * @param handler The SSL error handler responsible for cancelling the
+     * connection that resulted in an SSL error or proceeding per user request.
+     * @param error The SSL error object.
+     */
+    void showSSLCertificateOnError(
+            final WebView view, final SslErrorHandler handler,
+            final SslError error) {
+
+        SslCertificate cert = error.getCertificate();
+        if (cert == null) {
+            return;
+        }
+
+        mSSLCertificateOnErrorHandler = handler;
+        mSSLCertificateOnErrorView = view;
+        mSSLCertificateOnErrorError = error;
+        mSSLCertificateOnErrorDialog = createSslCertificateDialog(cert, error)
+                .setPositiveButton(R.string.ok,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog,
+                                    int whichButton) {
+                                mSSLCertificateOnErrorDialog = null;
+                                mSSLCertificateOnErrorView = null;
+                                mSSLCertificateOnErrorHandler = null;
+                                mSSLCertificateOnErrorError = null;
+
+                                ((BrowserWebView) view).getWebViewClient().
+                                        onReceivedSslError(view, handler, error);
+                            }
+                        })
+                 .setNeutralButton(R.string.page_info_view,
+                        new DialogInterface.OnClickListener() {
+                            public void onClick(DialogInterface dialog,
+                                    int whichButton) {
+                                mSSLCertificateOnErrorDialog = null;
+
+                                // do not clear the dialog state: we will
+                                // need to show the dialog again once the
+                                // user is done exploring the page-info details
+
+                                showPageInfo(mController.getTabControl()
+                                        .getTabFromView(view),
+                                        true,
+                                        error.getUrl());
+                            }
+                        })
+                .setOnCancelListener(
+                        new DialogInterface.OnCancelListener() {
+                            public void onCancel(DialogInterface dialog) {
+                                mSSLCertificateOnErrorDialog = null;
+                                mSSLCertificateOnErrorView = null;
+                                mSSLCertificateOnErrorHandler = null;
+                                mSSLCertificateOnErrorError = null;
+
+                                ((BrowserWebView) view).getWebViewClient().
+                                        onReceivedSslError(view, handler, error);
+                            }
+                        })
+                .show();
+    }
+
+    private static View inflateCertificateView(SslCertificate certificate, Context ctx) {
+        Class certClass;
+        try {
+            certClass = Class.forName("android.net.http.SslCertificate");
+
+            Class argTypes[] = new Class[1];
+            argTypes[0] = Context.class;
+
+            Method m =  certClass.getDeclaredMethod("inflateCertificateView", argTypes);
+            m.setAccessible(true);
+
+            Object args[] = new Object[1];
+            args[0] = ctx;
+            return (View) m.invoke(certificate, args);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+
+        return null;
+    }
+
+    /*
+     * Creates an AlertDialog to display the given certificate. If error is
+     * null, text is added to state that the certificae is valid and the icon
+     * is set accordingly. If error is non-null, it must relate to the supplied
+     * certificate. In this case, error is used to add text describing the
+     * problems with the certificate and a different icon is used.
+     */
+    private AlertDialog.Builder createSslCertificateDialog(SslCertificate certificate,
+            SslError error) {
+        View certificateView = inflateCertificateView(certificate, mContext);
+        Resources res = Resources.getSystem();
+        int placeholder_id = res.getIdentifier("placeholder", "id", "android");
+        final LinearLayout placeholder =
+                (LinearLayout)certificateView.findViewById(placeholder_id);
+
+        LayoutInflater factory = LayoutInflater.from(mContext);
+        int iconId;
+
+        if (error == null) {
+            iconId = R.drawable.ic_dialog_browser_certificate_secure;
+            LinearLayout table = (LinearLayout)factory.inflate(R.layout.ssl_success, placeholder);
+            TextView successString = (TextView)table.findViewById(R.id.success);
+            successString.setText(R.string.ssl_certificate_is_valid);
+        } else {
+            iconId = R.drawable.ic_dialog_browser_certificate_partially_secure;
+            if (error.hasError(SslError.SSL_UNTRUSTED)) {
+                addError(factory, placeholder, R.string.ssl_untrusted);
+            }
+            if (error.hasError(SslError.SSL_IDMISMATCH)) {
+                addError(factory, placeholder, R.string.ssl_mismatch);
+            }
+            if (error.hasError(SslError.SSL_EXPIRED)) {
+                addError(factory, placeholder, R.string.ssl_expired);
+            }
+            if (error.hasError(SslError.SSL_NOTYETVALID)) {
+                addError(factory, placeholder, R.string.ssl_not_yet_valid);
+            }
+            if (error.hasError(SslError.SSL_DATE_INVALID)) {
+                addError(factory, placeholder, R.string.ssl_date_invalid);
+            }
+            if (error.hasError(SslError.SSL_INVALID)) {
+                addError(factory, placeholder, R.string.ssl_invalid);
+            }
+            // The SslError should always have at least one type of error and we
+            // should explicitly handle every type of error it supports. We
+            // therefore expect the condition below to never be hit. We use it
+            // as as safety net in case a new error type is added to SslError
+            // without the logic above being updated accordingly.
+            if (placeholder.getChildCount() == 0) {
+                addError(factory, placeholder, R.string.ssl_unknown);
+            }
+        }
+
+        return new AlertDialog.Builder(mContext)
+                .setTitle(R.string.ssl_certificate)
+                .setIcon(iconId)
+                .setView(certificateView);
+    }
+
+    private void addError(LayoutInflater inflater, LinearLayout parent, int error) {
+        TextView textView = (TextView) inflater.inflate(R.layout.ssl_warning,
+                parent, false);
+        textView.setText(error);
+        parent.addView(textView);
+    }
+}
diff --git a/src/com/android/browser/PageProgressView.java b/src/com/android/browser/PageProgressView.java
new file mode 100644
index 0000000..f512cef
--- /dev/null
+++ b/src/com/android/browser/PageProgressView.java
@@ -0,0 +1,117 @@
+
+/*
+ * Copyright (C) 2010 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.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+/**
+ *
+ */
+public class PageProgressView extends ImageView {
+
+    public static final int MAX_PROGRESS = 10000;
+    private static final int MSG_UPDATE = 42;
+    private static final int STEPS = 10;
+    private static final int DELAY = 40;
+
+    private int mCurrentProgress;
+    private int mTargetProgress;
+    private int mIncrement;
+    private Rect mBounds;
+    private Handler mHandler;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public PageProgressView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public PageProgressView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /**
+     * @param context
+     */
+    public PageProgressView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        mBounds = new Rect(0,0,0,0);
+        mCurrentProgress = 0;
+        mTargetProgress = 0;
+        mHandler = new Handler() {
+
+            @Override
+            public void handleMessage(Message msg) {
+                if (msg.what == MSG_UPDATE) {
+                    mCurrentProgress = Math.min(mTargetProgress,
+                            mCurrentProgress + mIncrement);
+                    mBounds.right = getWidth() * mCurrentProgress / MAX_PROGRESS;
+                    invalidate();
+                    if (mCurrentProgress < mTargetProgress) {
+                        sendMessageDelayed(mHandler.obtainMessage(MSG_UPDATE), DELAY);
+                    }
+                }
+            }
+
+        };
+    }
+
+    @Override
+    public void onLayout(boolean f, int l, int t, int r, int b) {
+        mBounds.left = 0;
+        mBounds.right = (r - l) * mCurrentProgress / MAX_PROGRESS;
+        mBounds.top = 0;
+        mBounds.bottom = b-t;
+    }
+
+    void setProgress(int progress) {
+        mCurrentProgress = mTargetProgress;
+        mTargetProgress = progress;
+        mIncrement = (mTargetProgress - mCurrentProgress) / STEPS;
+        mHandler.removeMessages(MSG_UPDATE);
+        mHandler.sendEmptyMessage(MSG_UPDATE);
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+//        super.onDraw(canvas);
+        Drawable d = getDrawable();
+        d.setBounds(mBounds);
+        d.draw(canvas);
+    }
+
+}
diff --git a/src/com/android/browser/Performance.java b/src/com/android/browser/Performance.java
new file mode 100644
index 0000000..330f47e
--- /dev/null
+++ b/src/com/android/browser/Performance.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2010 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 com.android.browser.platformsupport.Process;
+import com.android.browser.platformsupport.WebAddress;
+
+import android.os.Debug;
+import android.os.SystemClock;
+import android.util.Log;
+
+/**
+ * Performance analysis
+ */
+public class Performance {
+
+    private static final String LOGTAG = "browser";
+
+    private final static boolean LOGD_ENABLED =
+            com.android.browser.Browser.LOGD_ENABLED;
+
+    private static boolean mInTrace;
+
+    // Performance probe
+    private static final int[] SYSTEM_CPU_FORMAT = new int[] {
+            Process.PROC_SPACE_TERM | Process.PROC_COMBINE,
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 1: user time
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 2: nice time
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 3: sys time
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 4: idle time
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 5: iowait time
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG, // 6: irq time
+            Process.PROC_SPACE_TERM | Process.PROC_OUT_LONG  // 7: softirq time
+    };
+
+    private static long mStart;
+    private static long mProcessStart;
+    private static long mUserStart;
+    private static long mSystemStart;
+    private static long mIdleStart;
+    private static long mIrqStart;
+
+    private static long mUiStart;
+
+    static void tracePageStart(String url) {
+        if (BrowserSettings.getInstance().isTracing()) {
+            String host;
+            try {
+                WebAddress uri = new WebAddress(url);
+                host = uri.getHost();
+            } catch (android.net.ParseException ex) {
+                host = "browser";
+            }
+            host = host.replace('.', '_');
+            host += ".trace";
+            mInTrace = true;
+            Debug.startMethodTracing(host, 20 * 1024 * 1024);
+        }
+    }
+
+    static void tracePageFinished() {
+        if (mInTrace) {
+            mInTrace = false;
+            Debug.stopMethodTracing();
+        }
+    }
+
+    static void onPageStarted() {
+        mStart = SystemClock.uptimeMillis();
+        mProcessStart = Process.getElapsedCpuTime();
+        long[] sysCpu = new long[7];
+        if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, sysCpu, null)) {
+            mUserStart = sysCpu[0] + sysCpu[1];
+            mSystemStart = sysCpu[2];
+            mIdleStart = sysCpu[3];
+            mIrqStart = sysCpu[4] + sysCpu[5] + sysCpu[6];
+        }
+        mUiStart = SystemClock.currentThreadTimeMillis();
+    }
+
+    static void onPageFinished(String url) {
+        long[] sysCpu = new long[7];
+        if (Process.readProcFile("/proc/stat", SYSTEM_CPU_FORMAT, null, sysCpu, null)) {
+            String uiInfo =
+                    "UI thread used " + (SystemClock.currentThreadTimeMillis() - mUiStart) + " ms";
+            if (LOGD_ENABLED) {
+                Log.d(LOGTAG, uiInfo);
+            }
+            // The string that gets written to the log
+            String performanceString =
+                    "It took total " + (SystemClock.uptimeMillis() - mStart)
+                            + " ms clock time to load the page." + "\nbrowser process used "
+                            + (Process.getElapsedCpuTime() - mProcessStart)
+                            + " ms, user processes used " + (sysCpu[0] + sysCpu[1] - mUserStart)
+                            * 10 + " ms, kernel used " + (sysCpu[2] - mSystemStart) * 10
+                            + " ms, idle took " + (sysCpu[3] - mIdleStart) * 10
+                            + " ms and irq took " + (sysCpu[4] + sysCpu[5] + sysCpu[6] - mIrqStart)
+                            * 10 + " ms, " + uiInfo;
+            if (LOGD_ENABLED) {
+                Log.d(LOGTAG, performanceString + "\nWebpage: " + url);
+            }
+            if (url != null) {
+                // strip the url to maintain consistency
+                String newUrl = new String(url);
+                if (newUrl.startsWith("http://www.")) {
+                    newUrl = newUrl.substring(11);
+                } else if (newUrl.startsWith("http://")) {
+                    newUrl = newUrl.substring(7);
+                } else if (newUrl.startsWith("https://www.")) {
+                    newUrl = newUrl.substring(12);
+                } else if (newUrl.startsWith("https://")) {
+                    newUrl = newUrl.substring(8);
+                }
+                if (LOGD_ENABLED) {
+                    Log.d(LOGTAG, newUrl + " loaded");
+                }
+            }
+        }
+    }
+}
diff --git a/src/com/android/browser/PhoneUi.java b/src/com/android/browser/PhoneUi.java
new file mode 100644
index 0000000..01488e1
--- /dev/null
+++ b/src/com/android/browser/PhoneUi.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright (C) 2010 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.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.os.Message;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import org.codeaurora.swe.WebView;
+import android.widget.ImageView;
+
+import com.android.browser.R;
+import com.android.browser.UrlInputView.StateListener;
+
+/**
+ * Ui for regular phone screen sizes
+ */
+public class PhoneUi extends BaseUi {
+
+    private static final String LOGTAG = "PhoneUi";
+    private static final int MSG_INIT_NAVSCREEN = 100;
+
+    private NavScreen mNavScreen;
+    private AnimScreen mAnimScreen;
+    private NavigationBarPhone mNavigationBar;
+    private int mActionBarHeight;
+
+    boolean mAnimating;
+    boolean mShowNav = false;
+
+    /**
+     * @param browser
+     * @param controller
+     */
+    public PhoneUi(Activity browser, UiController controller) {
+        super(browser, controller);
+        setUseQuickControls(BrowserSettings.getInstance().useQuickControls());
+        mNavigationBar = (NavigationBarPhone) mTitleBar.getNavigationBar();
+        TypedValue heightValue = new TypedValue();
+        browser.getTheme().resolveAttribute(
+                android.R.attr.actionBarSize, heightValue, true);
+        mActionBarHeight = TypedValue.complexToDimensionPixelSize(heightValue.data,
+                browser.getResources().getDisplayMetrics());
+    }
+
+    @Override
+    public void onDestroy() {
+        hideTitleBar();
+    }
+
+    @Override
+    public void editUrl(boolean clearInput, boolean forceIME) {
+        if (mUseQuickControls) {
+            mTitleBar.setShowProgressOnly(false);
+        }
+        //Do nothing while at Nav show screen.
+        if (mShowNav) return;
+        super.editUrl(clearInput, forceIME);
+    }
+
+    @Override
+    public boolean onBackKey() {
+        if (showingNavScreen()) {
+            mNavScreen.close(mUiController.getTabControl().getCurrentPosition());
+            return true;
+        }
+        return super.onBackKey();
+    }
+
+    private boolean showingNavScreen() {
+        return mNavScreen != null && mNavScreen.getVisibility() == View.VISIBLE;
+    }
+
+    @Override
+    public boolean dispatchKey(int code, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public void onProgressChanged(Tab tab) {
+        super.onProgressChanged(tab);
+        if (mNavScreen == null && getTitleBar().getHeight() > 0) {
+            mHandler.sendEmptyMessage(MSG_INIT_NAVSCREEN);
+        }
+    }
+
+    @Override
+    protected void handleMessage(Message msg) {
+        super.handleMessage(msg);
+        if (msg.what == MSG_INIT_NAVSCREEN) {
+            if (mNavScreen == null) {
+                mNavScreen = new NavScreen(mActivity, mUiController, this);
+                mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS);
+                mNavScreen.setVisibility(View.GONE);
+            }
+            if (mAnimScreen == null) {
+                mAnimScreen = new AnimScreen(mActivity);
+                // initialize bitmaps
+                mAnimScreen.set(getTitleBar(), getWebView());
+            }
+        }
+    }
+
+    @Override
+    public void setActiveTab(final Tab tab) {
+        mTitleBar.cancelTitleBarAnimation(true);
+        mTitleBar.setSkipTitleBarAnimations(true);
+        super.setActiveTab(tab);
+
+        //if at Nav screen show, detach tab like what showNavScreen() do.
+        if (mShowNav) {
+            detachTab(mActiveTab);
+        }
+
+        BrowserWebView view = (BrowserWebView) tab.getWebView();
+        // TabControl.setCurrentTab has been called before this,
+        // so the tab is guaranteed to have a webview
+        if (view == null) {
+            Log.e(LOGTAG, "active tab with no webview detected");
+            return;
+        }
+        // Request focus on the top window.
+        if (mUseQuickControls) {
+            mPieControl.forceToTop(mContentView);
+            view.setTitleBar(null);
+            mTitleBar.setShowProgressOnly(true);
+        } else {
+            view.setTitleBar(mTitleBar);
+        }
+        // update nav bar state
+        mNavigationBar.onStateChanged(StateListener.STATE_NORMAL);
+        updateLockIconToLatest(tab);
+        mTitleBar.setSkipTitleBarAnimations(false);
+    }
+
+    // menu handling callbacks
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        updateMenuState(mActiveTab, menu);
+        return true;
+    }
+
+    @Override
+    public void updateMenuState(Tab tab, Menu menu) {
+        MenuItem bm = menu.findItem(R.id.bookmarks_menu_id);
+        if (bm != null) {
+            bm.setVisible(!showingNavScreen());
+        }
+        MenuItem abm = menu.findItem(R.id.add_bookmark_menu_id);
+        if (abm != null) {
+            abm.setVisible((tab != null) && !tab.isSnapshot() && !showingNavScreen());
+        }
+        MenuItem info = menu.findItem(R.id.page_info_menu_id);
+        if (info != null) {
+            info.setVisible(false);
+        }
+        MenuItem newtab = menu.findItem(R.id.new_tab_menu_id);
+        if (newtab != null && !mUseQuickControls) {
+            newtab.setVisible(false);
+        }
+        MenuItem incognito = menu.findItem(R.id.incognito_menu_id);
+        if (incognito != null) {
+            incognito.setVisible(showingNavScreen() || mUseQuickControls);
+        }
+        MenuItem closeOthers = menu.findItem(R.id.close_other_tabs_id);
+        if (closeOthers != null) {
+            boolean isLastTab = true;
+            if (tab != null) {
+                isLastTab = (mTabControl.getTabCount() <= 1);
+            }
+            closeOthers.setEnabled(!isLastTab);
+        }
+        if (showingNavScreen()) {
+            menu.setGroupVisible(R.id.LIVE_MENU, false);
+            menu.setGroupVisible(R.id.SNAPSHOT_MENU, false);
+            menu.setGroupVisible(R.id.NAV_MENU, false);
+            menu.setGroupVisible(R.id.COMBO_MENU, true);
+        }
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        if (showingNavScreen()
+                && (item.getItemId() != R.id.history_menu_id)
+                && (item.getItemId() != R.id.snapshots_menu_id)) {
+            hideNavScreen(mUiController.getTabControl().getCurrentPosition(), false);
+        }
+        return false;
+    }
+
+    @Override
+    public void onContextMenuCreated(Menu menu) {
+        hideTitleBar();
+    }
+
+    @Override
+    public void onContextMenuClosed(Menu menu, boolean inLoad) {
+        if (inLoad) {
+            showTitleBar();
+        }
+    }
+
+    // action mode callbacks
+
+    @Override
+    public void onActionModeStarted(ActionMode mode) {
+        if (!isEditingUrl()) {
+            hideTitleBar();
+        } else {
+            mTitleBar.animate().translationY(mActionBarHeight);
+        }
+    }
+
+    @Override
+    public void onActionModeFinished(boolean inLoad) {
+        mTitleBar.animate().translationY(0);
+        if (inLoad) {
+            if (mUseQuickControls) {
+                mTitleBar.setShowProgressOnly(true);
+            }
+            showTitleBar();
+        }
+    }
+
+    @Override
+    public boolean isWebShowing() {
+        return super.isWebShowing() && !showingNavScreen();
+    }
+
+    @Override
+    public void showWeb(boolean animate) {
+        super.showWeb(animate);
+        hideNavScreen(mUiController.getTabControl().getCurrentPosition(), animate);
+    }
+
+    void showNavScreen() {
+        mShowNav = true;
+        mUiController.setBlockEvents(true);
+        if (mNavScreen == null) {
+            mNavScreen = new NavScreen(mActivity, mUiController, this);
+            mCustomViewContainer.addView(mNavScreen, COVER_SCREEN_PARAMS);
+        } else {
+            mNavScreen.setVisibility(View.VISIBLE);
+            mNavScreen.setAlpha(1f);
+            mNavScreen.refreshAdapter();
+        }
+        mActiveTab.capture();
+        if (mAnimScreen == null) {
+            mAnimScreen = new AnimScreen(mActivity);
+        } else {
+            mAnimScreen.mMain.setAlpha(1f);
+            mAnimScreen.mTitle.setAlpha(1f);
+            mAnimScreen.setScaleFactor(1f);
+        }
+        mAnimScreen.set(getTitleBar(), getWebView());
+        if (mAnimScreen.mMain.getParent() == null) {
+            mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS);
+        }
+        mCustomViewContainer.setVisibility(View.VISIBLE);
+        mCustomViewContainer.bringToFront();
+        mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(),
+                mContentView.getHeight());
+        int fromLeft = 0;
+        int fromTop = getTitleBar().getHeight();
+        int fromRight = mContentView.getWidth();
+        int fixedTbarHeight = mTitleBar.isFixed() ? mTitleBar.calculateEmbeddedHeight() : 0;
+        int fromBottom = mContentView.getHeight() + fixedTbarHeight;
+        int width = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_width);
+        int height = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_height);
+        int ntth = mActivity.getResources().getDimensionPixelSize(R.dimen.nav_tab_titleheight);
+        int toLeft = (mContentView.getWidth() - width) / 2;
+        int toTop = ((fromBottom - (ntth + height)) / 2 + ntth);
+        int toRight = toLeft + width;
+        int toBottom = toTop + height;
+        float scaleFactor = width / (float) mContentView.getWidth();
+        // SWE: Detaching the active tab results flashing screen with SWE.
+        // Not detaching the tab doesn't seem to have any issues.
+        //detachTab(mActiveTab);
+        mContentView.setVisibility(View.GONE);
+        AnimatorSet set1 = new AnimatorSet();
+        AnimatorSet inanim = new AnimatorSet();
+        ObjectAnimator tx = ObjectAnimator.ofInt(mAnimScreen.mContent, "left",
+                fromLeft, toLeft);
+        ObjectAnimator ty = ObjectAnimator.ofInt(mAnimScreen.mContent, "top",
+                fromTop, toTop);
+        ObjectAnimator tr = ObjectAnimator.ofInt(mAnimScreen.mContent, "right",
+                fromRight, toRight);
+        ObjectAnimator tb = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom",
+                fromBottom, toBottom);
+        ObjectAnimator title = ObjectAnimator.ofFloat(mAnimScreen.mTitle, "alpha",
+                1f, 0f);
+        ObjectAnimator sx = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor",
+                1f, scaleFactor);
+        ObjectAnimator blend1 = ObjectAnimator.ofFloat(mAnimScreen.mMain,
+                "alpha", 1f, 0f);
+        blend1.setDuration(100);
+
+        inanim.playTogether(tx, ty, tr, tb, sx, title);
+        inanim.setDuration(200);
+        set1.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator anim) {
+                mCustomViewContainer.removeView(mAnimScreen.mMain);
+                finishAnimationIn();
+                mUiController.setBlockEvents(false);
+            }
+        });
+        set1.playSequentially(inanim, blend1);
+        set1.start();
+    }
+
+    private void finishAnimationIn() {
+        if (showingNavScreen()) {
+            // notify accessibility manager about the screen change
+            mNavScreen.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+            mTabControl.setOnThumbnailUpdatedListener(mNavScreen);
+        }
+    }
+
+    void hideNavScreen(int position, boolean animate) {
+        mShowNav = false;
+        if (!showingNavScreen()) return;
+        final Tab tab = mUiController.getTabControl().getTab(position);
+        if ((tab == null) || !animate) {
+            if (tab != null) {
+                setActiveTab(tab);
+            } else if (mTabControl.getTabCount() > 0) {
+                // use a fallback tab
+                setActiveTab(mTabControl.getCurrentTab());
+            }
+            mContentView.setVisibility(View.VISIBLE);
+            finishAnimateOut();
+            return;
+        }
+        NavTabView tabview = (NavTabView) mNavScreen.getTabView(position);
+        if (tabview == null) {
+            if (mTabControl.getTabCount() > 0) {
+                // use a fallback tab
+                setActiveTab(mTabControl.getCurrentTab());
+            }
+            mContentView.setVisibility(View.VISIBLE);
+            finishAnimateOut();
+            return;
+        }
+        mUiController.setBlockEvents(true);
+        mUiController.setActiveTab(tab);
+        mContentView.setVisibility(View.VISIBLE);
+        if (mAnimScreen == null) {
+            mAnimScreen = new AnimScreen(mActivity);
+        }
+        mAnimScreen.set(tab.getScreenshot());
+        if (mAnimScreen.mMain.getParent() == null) {
+            mCustomViewContainer.addView(mAnimScreen.mMain, COVER_SCREEN_PARAMS);
+        }
+        int fixedTbarHeight = mTitleBar.isFixed() ? mTitleBar.calculateEmbeddedHeight() : 0;
+        mAnimScreen.mMain.layout(0, 0, mContentView.getWidth(),
+                mContentView.getHeight()  + fixedTbarHeight);
+        mNavScreen.mScroller.finishScroller();
+        ImageView target = tabview.mImage;
+        int toLeft = 0;
+        int toTop = 0;
+        if (mTitleBar.isFixed()) {
+            toTop = fixedTbarHeight;
+        } else {
+            toTop = (tab.getWebView() != null) ? tab.getWebView().getVisibleTitleHeight() : 0;
+        }
+        int toRight = mContentView.getWidth();
+        int width = target.getDrawable().getIntrinsicWidth();
+        int height = target.getDrawable().getIntrinsicHeight();
+        int fromLeft = tabview.getLeft() + target.getLeft() - mNavScreen.mScroller.getScrollX();
+        int fromTop = tabview.getTop() + target.getTop() - mNavScreen.mScroller.getScrollY();
+        int fromRight = fromLeft + width;
+        int fromBottom = fromTop + height;
+        float scaleFactor = mContentView.getWidth() / (float) width;
+        int toBottom = toTop + (int) (height * scaleFactor);
+        mAnimScreen.mContent.setLeft(fromLeft);
+        mAnimScreen.mContent.setTop(fromTop);
+        mAnimScreen.mContent.setRight(fromRight);
+        mAnimScreen.mContent.setBottom(fromBottom);
+        mAnimScreen.setScaleFactor(1f);
+        AnimatorSet set1 = new AnimatorSet();
+        ObjectAnimator fade2 = ObjectAnimator.ofFloat(mAnimScreen.mMain, "alpha", 0f, 1f);
+        ObjectAnimator fade1 = ObjectAnimator.ofFloat(mNavScreen, "alpha", 1f, 0f);
+        set1.playTogether(fade1, fade2);
+        set1.setDuration(100);
+        AnimatorSet set2 = new AnimatorSet();
+        ObjectAnimator l = ObjectAnimator.ofInt(mAnimScreen.mContent, "left",
+                fromLeft, toLeft);
+        ObjectAnimator t = ObjectAnimator.ofInt(mAnimScreen.mContent, "top",
+                fromTop, toTop);
+        ObjectAnimator r = ObjectAnimator.ofInt(mAnimScreen.mContent, "right",
+                fromRight, toRight);
+        ObjectAnimator b = ObjectAnimator.ofInt(mAnimScreen.mContent, "bottom",
+                fromBottom, toBottom);
+        ObjectAnimator scale = ObjectAnimator.ofFloat(mAnimScreen, "scaleFactor",
+                1f, scaleFactor);
+        ObjectAnimator otheralpha = ObjectAnimator.ofFloat(mCustomViewContainer, "alpha", 1f, 0f);
+        otheralpha.setDuration(100);
+        set2.playTogether(l, t, r, b, scale);
+        set2.setDuration(200);
+        AnimatorSet combo = new AnimatorSet();
+        combo.playSequentially(set1, set2, otheralpha);
+        combo.addListener(new AnimatorListenerAdapter() {
+            @Override
+            public void onAnimationEnd(Animator anim) {
+                mCustomViewContainer.removeView(mAnimScreen.mMain);
+                finishAnimateOut();
+                mUiController.setBlockEvents(false);
+            }
+        });
+        combo.start();
+    }
+
+    private void finishAnimateOut() {
+        mTabControl.setOnThumbnailUpdatedListener(null);
+        mNavScreen.setVisibility(View.GONE);
+        mCustomViewContainer.setAlpha(1f);
+        mCustomViewContainer.setVisibility(View.GONE);
+    }
+
+    @Override
+    public boolean needsRestoreAllTabs() {
+        return false;
+    }
+
+    public void toggleNavScreen() {
+        if (!showingNavScreen()) {
+            showNavScreen();
+        } else {
+            hideNavScreen(mUiController.getTabControl().getCurrentPosition(), false);
+        }
+    }
+
+    @Override
+    public boolean shouldCaptureThumbnails() {
+        return true;
+    }
+
+    static class AnimScreen {
+
+        private View mMain;
+        private ImageView mTitle;
+        private ImageView mContent;
+        private float mScale;
+        private Bitmap mTitleBarBitmap;
+        private Bitmap mContentBitmap;
+
+        public AnimScreen(Context ctx) {
+            mMain = LayoutInflater.from(ctx).inflate(R.layout.anim_screen,
+                    null);
+            mTitle = (ImageView) mMain.findViewById(R.id.title);
+            mContent = (ImageView) mMain.findViewById(R.id.content);
+            mContent.setScaleType(ImageView.ScaleType.MATRIX);
+            mContent.setImageMatrix(new Matrix());
+            mScale = 1.0f;
+            setScaleFactor(getScaleFactor());
+        }
+
+        public void set(TitleBar tbar, WebView web) {
+            if (tbar == null || web == null) {
+                return;
+            }
+            int embTbarHeight = tbar.getEmbeddedHeight();
+            int tbarHeight = tbar.isFixed() ? tbar.calculateEmbeddedHeight() : embTbarHeight;
+            if (tbar.getWidth() > 0 && tbarHeight > 0) {
+                if (mTitleBarBitmap == null
+                        || mTitleBarBitmap.getWidth() != tbar.getWidth()
+                        || mTitleBarBitmap.getHeight() != tbarHeight) {
+                    mTitleBarBitmap = safeCreateBitmap(tbar.getWidth(),
+                            tbarHeight);
+                }
+                if (mTitleBarBitmap != null) {
+                    Canvas c = new Canvas(mTitleBarBitmap);
+                    tbar.draw(c);
+                    c.setBitmap(null);
+                }
+            } else {
+                mTitleBarBitmap = null;
+            }
+            mTitle.setImageBitmap(mTitleBarBitmap);
+            mTitle.setVisibility(View.VISIBLE);
+            // SWE: WebView.draw() wouldn't draw anything if SurfaceView is enabled.
+            mContentBitmap = web.getViewportBitmap();
+            if (mContentBitmap == null) {
+                int h = web.getHeight() - embTbarHeight;
+                if (mContentBitmap == null
+                        || mContentBitmap.getWidth() != web.getWidth()
+                        || mContentBitmap.getHeight() != h) {
+                    mContentBitmap = safeCreateBitmap(web.getWidth(), h);
+                }
+                if (mContentBitmap != null) {
+                        Canvas c = new Canvas(mContentBitmap);
+                        int tx = web.getScrollX();
+                        int ty = web.getScrollY();
+                        c.translate(-tx, -ty - embTbarHeight);
+                        web.draw(c);
+                        c.setBitmap(null);
+                }
+            }
+            mContent.setImageBitmap(mContentBitmap);
+        }
+
+        private Bitmap safeCreateBitmap(int width, int height) {
+            if (width <= 0 || height <= 0) {
+                Log.w(LOGTAG, "safeCreateBitmap failed! width: " + width
+                        + ", height: " + height);
+                return null;
+            }
+            return Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+        }
+
+        public void set(Bitmap image) {
+            mTitle.setVisibility(View.GONE);
+            mContent.setImageBitmap(image);
+        }
+
+        private void setScaleFactor(float sf) {
+            mScale = sf;
+            Matrix m = new Matrix();
+            m.postScale(sf,sf);
+            mContent.setImageMatrix(m);
+        }
+
+        private float getScaleFactor() {
+            return mScale;
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/PieControl.java b/src/com/android/browser/PieControl.java
new file mode 100644
index 0000000..68f7983
--- /dev/null
+++ b/src/com/android/browser/PieControl.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright (C) 2011 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.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import org.codeaurora.swe.WebView;
+import android.widget.BaseAdapter;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+import com.android.browser.view.PieItem;
+import com.android.browser.view.PieMenu;
+import com.android.browser.view.PieStackView;
+import com.android.browser.view.PieMenu.PieView.OnLayoutListener;
+import com.android.browser.view.PieStackView.OnCurrentListener;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Controller for Quick Controls pie menu
+ */
+public class PieControl implements PieMenu.PieController, OnClickListener {
+
+    protected Activity mActivity;
+    protected UiController mUiController;
+    protected PieMenu mPie;
+    protected int mItemSize;
+    protected TextView mTabsCount;
+    private BaseUi mUi;
+    private PieItem mBack;
+    private PieItem mForward;
+    private PieItem mRefresh;
+    private PieItem mUrl;
+    private PieItem mOptions;
+    private PieItem mBookmarks;
+    private PieItem mHistory;
+    private PieItem mAddBookmark;
+    private PieItem mNewTab;
+    private PieItem mIncognito;
+    private PieItem mClose;
+    private PieItem mShowTabs;
+    private PieItem mInfo;
+    private PieItem mFind;
+    private PieItem mShare;
+    private PieItem mRDS;
+    private TabAdapter mTabAdapter;
+
+    public PieControl(Activity activity, UiController controller, BaseUi ui) {
+        mActivity = activity;
+        mUiController = controller;
+        mItemSize = (int) activity.getResources().getDimension(R.dimen.qc_item_size);
+        mUi = ui;
+    }
+
+    public void stopEditingUrl() {
+        mUi.stopEditingUrl();
+    }
+
+    protected void attachToContainer(FrameLayout container) {
+        if (mPie == null) {
+            mPie = new PieMenu(mActivity);
+            LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT,
+                    LayoutParams.MATCH_PARENT);
+            mPie.setLayoutParams(lp);
+            populateMenu();
+            mPie.setController(this);
+        }
+        container.addView(mPie);
+    }
+
+    protected void removeFromContainer(FrameLayout container) {
+        container.removeView(mPie);
+    }
+
+    protected void forceToTop(FrameLayout container) {
+        if (mPie.getParent() != null) {
+            container.removeView(mPie);
+            container.addView(mPie);
+        }
+    }
+
+    protected void setClickListener(OnClickListener listener, PieItem... items) {
+        for (PieItem item : items) {
+            item.getView().setOnClickListener(listener);
+        }
+    }
+
+    @Override
+    public boolean onOpen() {
+        int n = mUiController.getTabControl().getTabCount();
+        mTabsCount.setText(Integer.toString(n));
+        Tab tab = mUiController.getCurrentTab();
+        if (tab != null) {
+            mForward.setEnabled(tab.canGoForward());
+        }
+        WebView view = mUiController.getCurrentWebView();
+        if (view != null) {
+            ImageView icon = (ImageView) mRDS.getView();
+            if (mUiController.getSettings().hasDesktopUseragent(view)) {
+                icon.setImageResource(R.drawable.ic_mobile);
+            } else {
+                icon.setImageResource(R.drawable.ic_desktop_holo_dark);
+            }
+        }
+        return true;
+    }
+
+    protected void populateMenu() {
+        mBack = makeItem(R.drawable.ic_back_holo_dark, 1);
+        mUrl = makeItem(R.drawable.ic_web_holo_dark, 1);
+        mBookmarks = makeItem(R.drawable.ic_bookmarks_holo_dark, 1);
+        mHistory = makeItem(R.drawable.ic_history_holo_dark, 1);
+        mAddBookmark = makeItem(R.drawable.ic_bookmark_on_holo_dark, 1);
+        mRefresh = makeItem(R.drawable.ic_refresh_holo_dark, 1);
+        mForward = makeItem(R.drawable.ic_forward_holo_dark, 1);
+        mNewTab = makeItem(R.drawable.ic_new_window_holo_dark, 1);
+        mIncognito = makeItem(R.drawable.ic_new_incognito_holo_dark, 1);
+        mClose = makeItem(R.drawable.ic_close_window_holo_dark, 1);
+        mInfo = makeItem(android.R.drawable.ic_menu_info_details, 1);
+        mFind = makeItem(R.drawable.ic_search_holo_dark, 1);
+        mShare = makeItem(R.drawable.ic_share_holo_dark, 1);
+        View tabs = makeTabsView();
+        mShowTabs = new PieItem(tabs, 1);
+        mOptions = makeItem(R.drawable.ic_settings_holo_dark, 1);
+        mRDS = makeItem(R.drawable.ic_desktop_holo_dark, 1);
+        mTabAdapter = new TabAdapter(mActivity, mUiController);
+        PieStackView stack = new PieStackView(mActivity);
+        stack.setLayoutListener(new OnLayoutListener() {
+            @Override
+            public void onLayout(int ax, int ay, boolean left) {
+                buildTabs();
+            }
+        });
+        stack.setOnCurrentListener(mTabAdapter);
+        stack.setAdapter(mTabAdapter);
+        mShowTabs.setPieView(stack);
+        setClickListener(this, mBack, mRefresh, mForward, mUrl, mFind, mInfo,
+                mShare, mBookmarks, mNewTab, mIncognito, mClose, mHistory,
+                mAddBookmark, mOptions, mRDS);
+        if (!BrowserActivity.isTablet(mActivity)) {
+            mShowTabs.getView().setOnClickListener(this);
+        }
+        // level 1
+        mPie.addItem(mOptions);
+        mOptions.addItem(mRDS);
+        mOptions.addItem(makeFiller());
+        mOptions.addItem(makeFiller());
+        mOptions.addItem(makeFiller());
+        mPie.addItem(mBack);
+        mBack.addItem(mRefresh);
+        mBack.addItem(mForward);
+        mBack.addItem(makeFiller());
+        mBack.addItem(makeFiller());
+        mPie.addItem(mUrl);
+        mUrl.addItem(mFind);
+        mUrl.addItem(mShare);
+        mUrl.addItem(makeFiller());
+        mUrl.addItem(makeFiller());
+        mPie.addItem(mShowTabs);
+        mShowTabs.addItem(mClose);
+        mShowTabs.addItem(mIncognito);
+        mShowTabs.addItem(mNewTab);
+        mShowTabs.addItem(makeFiller());
+        mPie.addItem(mBookmarks);
+        mBookmarks.addItem(makeFiller());
+        mBookmarks.addItem(makeFiller());
+        mBookmarks.addItem(mAddBookmark);
+        mBookmarks.addItem(mHistory);
+    }
+
+    @Override
+    public void onClick(View v) {
+        Tab tab = mUiController.getTabControl().getCurrentTab();
+        WebView web = tab.getWebView();
+        if (mBack.getView() == v) {
+            tab.goBack();
+        } else if (mForward.getView() == v) {
+            tab.goForward();
+        } else if (mRefresh.getView() == v) {
+            if (tab.inPageLoad()) {
+                web.stopLoading();
+            } else {
+                web.reload();
+            }
+        } else if (mUrl.getView() == v) {
+            mUi.editUrl(false, true);
+        } else if (mBookmarks.getView() == v) {
+            mUiController.bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+        } else if (mHistory.getView() == v) {
+            mUiController.bookmarksOrHistoryPicker(ComboViews.History);
+        } else if (mAddBookmark.getView() == v) {
+            mUiController.bookmarkCurrentPage();
+        } else if (mNewTab.getView() == v) {
+            mUiController.openTabToHomePage();
+            mUi.editUrl(false, true);
+        } else if (mIncognito.getView() == v) {
+            mUiController.openIncognitoTab();
+            mUi.editUrl(false, true);
+        } else if (mClose.getView() == v) {
+            mUiController.closeCurrentTab();
+        } else if (mOptions.getView() == v) {
+            mUiController.openPreferences();
+        } else if (mShare.getView() == v) {
+            mUiController.shareCurrentPage();
+        } else if (mInfo.getView() == v) {
+            mUiController.showPageInfo();
+        } else if (mFind.getView() == v) {
+            mUiController.findOnPage();
+        } else if (mRDS.getView() == v) {
+            mUiController.toggleUserAgent();
+        } else if (mShowTabs.getView() == v) {
+            ((PhoneUi) mUi).showNavScreen();
+        }
+    }
+
+    private void buildTabs() {
+        final List<Tab> tabs = mUiController.getTabs();
+        mUi.getActiveTab().capture();
+        mTabAdapter.setTabs(tabs);
+        PieStackView sym = (PieStackView) mShowTabs.getPieView();
+        sym.setCurrent(mUiController.getTabControl().getCurrentPosition());
+    }
+
+    protected PieItem makeItem(int image, int l) {
+        ImageView view = new ImageView(mActivity);
+        view.setImageResource(image);
+        view.setMinimumWidth(mItemSize);
+        view.setMinimumHeight(mItemSize);
+        view.setScaleType(ScaleType.CENTER);
+        LayoutParams lp = new LayoutParams(mItemSize, mItemSize);
+        view.setLayoutParams(lp);
+        return new PieItem(view, l);
+    }
+
+    protected PieItem makeFiller() {
+        return new PieItem(null, 1);
+    }
+
+    protected View makeTabsView() {
+        View v = mActivity.getLayoutInflater().inflate(R.layout.qc_tabs_view, null);
+        mTabsCount = (TextView) v.findViewById(R.id.label);
+        mTabsCount.setText("1");
+        ImageView image = (ImageView) v.findViewById(R.id.icon);
+        image.setImageResource(R.drawable.ic_windows_holo_dark);
+        image.setScaleType(ScaleType.CENTER);
+        LayoutParams lp = new LayoutParams(mItemSize, mItemSize);
+        v.setLayoutParams(lp);
+        return v;
+    }
+
+    static class TabAdapter extends BaseAdapter implements OnCurrentListener {
+
+        LayoutInflater mInflater;
+        UiController mUiController;
+        private List<Tab> mTabs;
+        private int mCurrent;
+
+        public TabAdapter(Context ctx, UiController ctl) {
+            mInflater = LayoutInflater.from(ctx);
+            mUiController = ctl;
+            mTabs = new ArrayList<Tab>();
+            mCurrent = -1;
+        }
+
+        public void setTabs(List<Tab> tabs) {
+            mTabs = tabs;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getCount() {
+            return mTabs.size();
+        }
+
+        @Override
+        public Tab getItem(int position) {
+            return mTabs.get(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final Tab tab = mTabs.get(position);
+            View view = mInflater.inflate(R.layout.qc_tab,
+                    null);
+            ImageView thumb = (ImageView) view.findViewById(R.id.thumb);
+            TextView title1 = (TextView) view.findViewById(R.id.title1);
+            TextView title2 = (TextView) view.findViewById(R.id.title2);
+            Bitmap b = tab.getScreenshot();
+            if (b != null) {
+                thumb.setImageBitmap(b);
+            }
+            if (position > mCurrent) {
+                title1.setVisibility(View.GONE);
+                title2.setText(tab.getTitle());
+            } else {
+                title2.setVisibility(View.GONE);
+                title1.setText(tab.getTitle());
+            }
+            view.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mUiController.switchToTab(tab);
+                }
+            });
+            return view;
+        }
+
+        @Override
+        public void onSetCurrent(int index) {
+            mCurrent = index;
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/PreferenceKeys.java b/src/com/android/browser/PreferenceKeys.java
new file mode 100644
index 0000000..8620053
--- /dev/null
+++ b/src/com/android/browser/PreferenceKeys.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface PreferenceKeys {
+
+    static final String PREF_AUTOFILL_ACTIVE_PROFILE_ID = "autofill_active_profile_id";
+    static final String PREF_DEBUG_MENU = "debug_menu";
+
+    // ----------------------
+    // Keys for accessibility_preferences.xml
+    // ----------------------
+    static final String PREF_MIN_FONT_SIZE = "min_font_size";
+    static final String PREF_TEXT_SIZE = "text_size";
+    static final String PREF_TEXT_ZOOM = "text_zoom";
+    static final String PREF_DOUBLE_TAP_ZOOM = "double_tap_zoom";
+    static final String PREF_FORCE_USERSCALABLE = "force_userscalable";
+    static final String PREF_INVERTED = "inverted";
+    static final String PREF_INVERTED_CONTRAST = "inverted_contrast";
+
+    // ----------------------
+    // Keys for advanced_preferences.xml
+    // ----------------------
+    static final String PREF_AUTOFIT_PAGES = "autofit_pages";
+    static final String PREF_BLOCK_POPUP_WINDOWS = "block_popup_windows";
+    static final String PREF_DEFAULT_TEXT_ENCODING = "default_text_encoding";
+    static final String PREF_DEFAULT_ZOOM = "default_zoom";
+    static final String PREF_ENABLE_JAVASCRIPT = "enable_javascript";
+    static final String PREF_ENABLE_MEMORY_MONITOR = "enable_memory_monitor";
+    static final String PREF_LOAD_PAGE = "load_page";
+    static final String PREF_OPEN_IN_BACKGROUND = "open_in_background";
+    static final String PREF_PLUGIN_STATE = "plugin_state";
+    static final String PREF_RESET_DEFAULT_PREFERENCES = "reset_default_preferences";
+    static final String PREF_SEARCH_ENGINE = "search_engine";
+    static final String PREF_WEBSITE_SETTINGS = "website_settings";
+    static final String PREF_ALLOW_APP_TABS = "allow_apptabs";
+    // Keys for download path settings
+    static final String PREF_DOWNLOAD_PATH = "download_path_setting_screen";
+    // ----------------------
+    // Keys for debug_preferences.xml
+    // ----------------------
+    static final String PREF_ENABLE_HARDWARE_ACCEL = "enable_hardware_accel";
+    static final String PREF_ENABLE_HARDWARE_ACCEL_SKIA = "enable_hardware_accel_skia";
+    static final String PREF_USER_AGENT = "user_agent";
+
+    // ----------------------
+    // Keys for general_preferences.xml
+    // ----------------------
+    static final String PREF_AUTOFILL_ENABLED = "autofill_enabled";
+    static final String PREF_AUTOFILL_PROFILE = "autofill_profile";
+    static final String PREF_HOMEPAGE = "homepage";
+    static final String PREF_SYNC_WITH_CHROME = "sync_with_chrome";
+
+    // ----------------------
+    // Keys for hidden_debug_preferences.xml
+    // ----------------------
+    static final String PREF_ENABLE_LIGHT_TOUCH = "enable_light_touch";
+    static final String PREF_ENABLE_NAV_DUMP = "enable_nav_dump";
+    static final String PREF_ENABLE_TRACING = "enable_tracing";
+    static final String PREF_ENABLE_VISUAL_INDICATOR = "enable_visual_indicator";
+    static final String PREF_ENABLE_CPU_UPLOAD_PATH = "enable_cpu_upload_path";
+    static final String PREF_JAVASCRIPT_CONSOLE = "javascript_console";
+    static final String PREF_JS_ENGINE_FLAGS = "js_engine_flags";
+    static final String PREF_NORMAL_LAYOUT = "normal_layout";
+    static final String PREF_SMALL_SCREEN = "small_screen";
+    static final String PREF_WIDE_VIEWPORT = "wide_viewport";
+    static final String PREF_RESET_PRELOGIN = "reset_prelogin";
+
+    // ----------------------
+    // Keys for lab_preferences.xml
+    // ----------------------
+    static final String PREF_ENABLE_QUICK_CONTROLS = "enable_quick_controls";
+    static final String PREF_FULLSCREEN = "fullscreen";
+
+    // ----------------------
+    // Keys for privacy_security_preferences.xml
+    // ----------------------
+    static final String PREF_ACCEPT_COOKIES = "accept_cookies";
+    static final String PREF_ENABLE_GEOLOCATION = "enable_geolocation";
+    static final String PREF_PRIVACY_CLEAR_CACHE = "privacy_clear_cache";
+    static final String PREF_PRIVACY_CLEAR_COOKIES = "privacy_clear_cookies";
+    static final String PREF_PRIVACY_CLEAR_FORM_DATA = "privacy_clear_form_data";
+    static final String PREF_PRIVACY_CLEAR_GEOLOCATION_ACCESS = "privacy_clear_geolocation_access";
+    static final String PREF_PRIVACY_CLEAR_HISTORY = "privacy_clear_history";
+    static final String PREF_PRIVACY_CLEAR_PASSWORDS = "privacy_clear_passwords";
+    static final String PREF_REMEMBER_PASSWORDS = "remember_passwords";
+    static final String PREF_SAVE_FORMDATA = "save_formdata";
+    static final String PREF_SHOW_SECURITY_WARNINGS = "show_security_warnings";
+
+    // ----------------------
+    // Keys for bandwidth_preferences.xml
+    // ----------------------
+    static final String PREF_DATA_PRELOAD = "preload_when";
+    static final String PREF_LINK_PREFETCH = "link_prefetch_when";
+    static final String PREF_LOAD_IMAGES = "load_images";
+
+    // ----------------------
+    // Keys for browser recovery
+    // ----------------------
+    /**
+     * The last time recovery was started as System.currentTimeMillis.
+     * 0 if not set.
+     */
+    static final String KEY_LAST_RECOVERED = "last_recovered";
+
+    /**
+     * Key for whether or not the last run was paused.
+     */
+    static final String KEY_LAST_RUN_PAUSED = "last_paused";
+}
diff --git a/src/com/android/browser/PreloadController.java b/src/com/android/browser/PreloadController.java
new file mode 100644
index 0000000..b564318
--- /dev/null
+++ b/src/com/android/browser/PreloadController.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2011 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.net.Uri;
+import android.net.http.SslError;
+import android.os.Message;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import org.codeaurora.swe.WebView;
+
+public class PreloadController implements WebViewController {
+
+    private static final boolean LOGD_ENABLED = false;
+    private static final String LOGTAG = "PreloadController";
+
+    private Context mContext;
+
+    public PreloadController(Context ctx) {
+        mContext = ctx.getApplicationContext();
+
+    }
+
+    @Override
+    public Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public Activity getActivity() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "getActivity()");
+        return null;
+    }
+
+    @Override
+    public TabControl getTabControl() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "getTabControl()");
+        return null;
+    }
+
+    @Override
+    public WebViewFactory getWebViewFactory() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "getWebViewFactory()");
+        return null;
+    }
+
+    @Override
+    public void onSetWebView(Tab tab, WebView view) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onSetWebView()");
+    }
+
+    @Override
+    public void createSubWindow(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "createSubWindow()");
+    }
+
+    @Override
+    public void onPageStarted(Tab tab, WebView view, Bitmap favicon) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onPageStarted()");
+        if (view != null) {
+            // Clear history of all previously visited pages. When the
+            // user visits a preloaded tab, the only item in the history
+            // list should the currently viewed page.
+            view.clearHistory();
+        }
+    }
+
+    @Override
+    public void onPageFinished(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onPageFinished()");
+        if (tab != null) {
+            final WebView view = tab.getWebView();
+            if (view != null) {
+                // Clear history of all previously visited pages. When the
+                // user visits a preloaded tab.
+                view.clearHistory();
+            }
+        }
+    }
+
+    @Override
+    public void onProgressChanged(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onProgressChanged()");
+    }
+
+    @Override
+    public void onReceivedTitle(Tab tab, String title) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onReceivedTitle()");
+    }
+
+    @Override
+    public void onFavicon(Tab tab, WebView view, Bitmap icon) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onFavicon()");
+    }
+
+    @Override
+    public boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "shouldOverrideUrlLoading()");
+        return false;
+    }
+
+    @Override
+    public boolean shouldOverrideKeyEvent(KeyEvent event) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "shouldOverrideKeyEvent()");
+        return false;
+    }
+
+    @Override
+    public boolean onUnhandledKeyEvent(KeyEvent event) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onUnhandledKeyEvent()");
+        return false;
+    }
+
+    @Override
+    public void doUpdateVisitedHistory(Tab tab, boolean isReload) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "doUpdateVisitedHistory()");
+    }
+
+    @Override
+    public void getVisitedHistory(ValueCallback<String[]> callback) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "getVisitedHistory()");
+    }
+
+    @Override
+    public void onReceivedHttpAuthRequest(Tab tab, WebView view,
+                                    HttpAuthHandler handler, String host,
+                                    String realm) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onReceivedHttpAuthRequest()");
+    }
+
+    @Override
+    public void onDownloadStart(Tab tab, String url, String useragent,
+                                    String contentDisposition, String mimeType,
+                                    String referer, long contentLength) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onDownloadStart()");
+    }
+
+    @Override
+    public void showCustomView(Tab tab, View view, int requestedOrientation,
+                                    CustomViewCallback callback) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "showCustomView()");
+    }
+
+    @Override
+    public void hideCustomView() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "hideCustomView()");
+    }
+
+    @Override
+    public Bitmap getDefaultVideoPoster() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "getDefaultVideoPoster()");
+        return null;
+    }
+
+    @Override
+    public View getVideoLoadingProgressView() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "getVideoLoadingProgressView()");
+        return null;
+    }
+
+    @Override
+    public void showSslCertificateOnError(WebView view,
+                                    SslErrorHandler handler, SslError error) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "showSslCertificateOnError()");
+    }
+
+    @Override
+    public void onUserCanceledSsl(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onUserCanceledSsl()");
+    }
+
+    @Override
+    public boolean shouldShowErrorConsole() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "shouldShowErrorConsole()");
+        return false;
+    }
+
+    @Override
+    public void onUpdatedSecurityState(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onUpdatedSecurityState()");
+    }
+
+    @Override
+    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "openFileChooser()");
+    }
+
+    @Override
+    public void endActionMode() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "endActionMode()");
+    }
+
+    @Override
+    public void attachSubWindow(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "attachSubWindow()");
+    }
+
+    @Override
+    public void dismissSubWindow(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "dismissSubWindow()");
+    }
+
+    @Override
+    public Tab openTab(String url, boolean incognito, boolean setActive, boolean useCurrent) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "openTab()");
+        return null;
+    }
+
+    @Override
+    public Tab openTab(String url, Tab parent, boolean setActive, boolean useCurrent) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "openTab()");
+        return null;
+    }
+
+    @Override
+    public boolean switchToTab(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "switchToTab()");
+        return false;
+    }
+
+    @Override
+    public void closeTab(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "closeTab()");
+    }
+
+    @Override
+    public void setupAutoFill(Message message) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "setupAutoFill()");
+    }
+
+    @Override
+    public void bookmarkedStatusHasChanged(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "bookmarkedStatusHasChanged()");
+    }
+
+    @Override
+    public void showAutoLogin(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "showAutoLogin()");
+    }
+
+    @Override
+    public void hideAutoLogin(Tab tab) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "hideAutoLogin()");
+    }
+
+    @Override
+    public boolean shouldCaptureThumbnails() {
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/PreloadRequestReceiver.java b/src/com/android/browser/PreloadRequestReceiver.java
new file mode 100644
index 0000000..c654037
--- /dev/null
+++ b/src/com/android/browser/PreloadRequestReceiver.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 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.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * Broadcast receiver for receiving browser preload requests
+ */
+public class PreloadRequestReceiver extends BroadcastReceiver {
+
+    private final static String LOGTAG = "browser.preloader";
+    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+
+    private static final String ACTION_PRELOAD = "android.intent.action.PRELOAD";
+    static final String EXTRA_PRELOAD_ID = "preload_id";
+    static final String EXTRA_PRELOAD_DISCARD = "preload_discard";
+    static final String EXTRA_SEARCHBOX_CANCEL = "searchbox_cancel";
+    static final String EXTRA_SEARCHBOX_SETQUERY = "searchbox_query";
+
+    private ConnectivityManager mConnectivityManager;
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "received intent " + intent);
+        if (isPreloadEnabledOnCurrentNetwork(context) &&
+                intent.getAction().equals(ACTION_PRELOAD)) {
+            handlePreload(context, intent);
+        }
+    }
+
+    private boolean isPreloadEnabledOnCurrentNetwork(Context context) {
+        String preload = BrowserSettings.getInstance().getPreloadEnabled();
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Preload setting: " + preload);
+        if (BrowserSettings.getPreloadAlwaysPreferenceString(context).equals(preload)) {
+            return true;
+        } else if (BrowserSettings.getPreloadOnWifiOnlyPreferenceString(context).equals(preload)) {
+            boolean onWifi = isOnWifi(context);
+            if (LOGD_ENABLED) Log.d(LOGTAG, "on wifi:" + onWifi);
+            return onWifi;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean isOnWifi(Context context) {
+        if (mConnectivityManager == null) {
+            mConnectivityManager = (ConnectivityManager)
+                    context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        }
+        NetworkInfo ni = mConnectivityManager.getActiveNetworkInfo();
+        if (ni == null) {
+            return false;
+        }
+        switch (ni.getType()) {
+            case ConnectivityManager.TYPE_MOBILE:
+            case ConnectivityManager.TYPE_MOBILE_DUN:
+            case ConnectivityManager.TYPE_MOBILE_MMS:
+            case ConnectivityManager.TYPE_MOBILE_SUPL:
+            case ConnectivityManager.TYPE_MOBILE_HIPRI:
+            case ConnectivityManager.TYPE_WIMAX: // separate case for this?
+                return false;
+            case ConnectivityManager.TYPE_WIFI:
+            case ConnectivityManager.TYPE_ETHERNET:
+            case ConnectivityManager.TYPE_BLUETOOTH:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private void handlePreload(Context context, Intent i) {
+        String url = UrlUtils.smartUrlFilter(i.getData());
+        String id = i.getStringExtra(EXTRA_PRELOAD_ID);
+        Map<String, String> headers = null;
+        if (id == null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Preload request has no " + EXTRA_PRELOAD_ID);
+            return;
+        }
+        if (i.getBooleanExtra(EXTRA_PRELOAD_DISCARD, false)) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " preload discard request");
+            Preloader.getInstance().discardPreload(id);
+        } else if (i.getBooleanExtra(EXTRA_SEARCHBOX_CANCEL, false)) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " searchbox cancel request");
+            Preloader.getInstance().cancelSearchBoxPreload(id);
+        } else {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Got " + id + " preload request for " + url);
+            if (url != null && url.startsWith("http")) {
+                final Bundle pairs = i.getBundleExtra(Browser.EXTRA_HEADERS);
+                if (pairs != null && !pairs.isEmpty()) {
+                    Iterator<String> iter = pairs.keySet().iterator();
+                    headers = new HashMap<String, String>();
+                    while (iter.hasNext()) {
+                        String key = iter.next();
+                        headers.put(key, pairs.getString(key));
+                    }
+                }
+            }
+            String sbQuery = i.getStringExtra(EXTRA_SEARCHBOX_SETQUERY);
+            if (url != null) {
+                if (LOGD_ENABLED){
+                    Log.d(LOGTAG, "Preload request(" + id + ", " + url + ", " +
+                            headers + ", " + sbQuery + ")");
+                }
+                Preloader.getInstance().handlePreloadRequest(id, url, headers, sbQuery);
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/browser/PreloadedTabControl.java b/src/com/android/browser/PreloadedTabControl.java
new file mode 100644
index 0000000..21dafa9
--- /dev/null
+++ b/src/com/android/browser/PreloadedTabControl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2011 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.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Class to manage the controlling of preloaded tab.
+ */
+public class PreloadedTabControl {
+    private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+    private static final String LOGTAG = "PreloadedTabControl";
+
+    final Tab mTab;
+    private String mLastQuery;
+    private boolean mDestroyed;
+
+    public PreloadedTabControl(Tab t) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "PreloadedTabControl.<init>");
+        mTab = t;
+    }
+
+    public void setQuery(String query) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Cannot set query: no searchbox interface");
+    }
+
+    public boolean searchBoxSubmit(final String query,
+            final String fallbackUrl, final Map<String, String> fallbackHeaders) {
+        return false;
+    }
+
+    public void searchBoxCancel() {
+    }
+
+    public void loadUrlIfChanged(String url, Map<String, String> headers) {
+        String currentUrl = mTab.getUrl();
+        if (!TextUtils.isEmpty(currentUrl)) {
+            try {
+                // remove fragment:
+                currentUrl = Uri.parse(currentUrl).buildUpon().fragment(null).build().toString();
+            } catch (UnsupportedOperationException e) {
+                // carry on
+            }
+        }
+        if (LOGD_ENABLED) Log.d(LOGTAG, "loadUrlIfChanged\nnew: " + url + "\nold: " +currentUrl);
+        if (!TextUtils.equals(url, currentUrl)) {
+            loadUrl(url, headers);
+        }
+    }
+
+    public void loadUrl(String url, Map<String, String> headers) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Preloading " + url);
+        mTab.loadUrl(url, headers);
+    }
+
+    public void destroy() {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "PreloadedTabControl.destroy");
+        mDestroyed = true;
+        mTab.destroy();
+    }
+
+    public Tab getTab() {
+        return mTab;
+    }
+
+}
diff --git a/src/com/android/browser/Preloader.java b/src/com/android/browser/Preloader.java
new file mode 100644
index 0000000..7d8c367
--- /dev/null
+++ b/src/com/android/browser/Preloader.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import org.codeaurora.swe.WebView;
+
+import java.util.Map;
+
+/**
+ * Singleton class for handling preload requests.
+ */
+public class Preloader {
+
+    private final static String LOGTAG = "browser.preloader";
+    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+
+    private static final int PRERENDER_TIMEOUT_MILLIS = 30 * 1000; // 30s
+
+    private static Preloader sInstance;
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final BrowserWebViewFactory mFactory;
+    private volatile PreloaderSession mSession;
+
+    public static void initialize(Context context) {
+        sInstance = new Preloader(context);
+    }
+
+    public static Preloader getInstance() {
+        return sInstance;
+    }
+
+    private Preloader(Context context) {
+        mContext = context.getApplicationContext();
+        mHandler = new Handler(Looper.getMainLooper());
+        mSession = null;
+        mFactory = new BrowserWebViewFactory(context);
+
+    }
+
+    private PreloaderSession getSession(String id) {
+        if (mSession == null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Create new preload session " + id);
+            mSession = new PreloaderSession(id);
+            WebViewTimersControl.getInstance().onPrerenderStart(
+                    mSession.getWebView());
+            return mSession;
+        } else if (mSession.mId.equals(id)) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Returning existing preload session " + id);
+            return mSession;
+        }
+
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Existing session in progress : " + mSession.mId +
+                " returning null.");
+        return null;
+    }
+
+    private PreloaderSession takeSession(String id) {
+        PreloaderSession s = null;
+        if (mSession != null && mSession.mId.equals(id)) {
+            s = mSession;
+            mSession = null;
+        }
+
+        if (s != null) {
+            s.cancelTimeout();
+        }
+
+        return s;
+    }
+
+    public void handlePreloadRequest(String id, String url, Map<String, String> headers,
+            String searchBoxQuery) {
+        PreloaderSession s = getSession(id);
+        if (s == null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Discarding preload request, existing"
+                    + " session in progress");
+            return;
+        }
+
+        s.touch(); // reset timer
+        PreloadedTabControl tab = s.getTabControl();
+        if (searchBoxQuery != null) {
+            tab.loadUrlIfChanged(url, headers);
+            tab.setQuery(searchBoxQuery);
+        } else {
+            tab.loadUrl(url, headers);
+        }
+    }
+
+    public void cancelSearchBoxPreload(String id) {
+        PreloaderSession s = getSession(id);
+        if (s != null) {
+            s.touch(); // reset timer
+            PreloadedTabControl tab = s.getTabControl();
+            tab.searchBoxCancel();
+        }
+    }
+
+    public void discardPreload(String id) {
+        PreloaderSession s = takeSession(id);
+        if (s != null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Discard preload session " + id);
+            WebViewTimersControl.getInstance().onPrerenderDone(s == null ? null : s.getWebView());
+            PreloadedTabControl t = s.getTabControl();
+            t.destroy();
+        } else {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Ignored discard request " + id);
+        }
+    }
+
+    /**
+     * Return a preloaded tab, and remove it from the preloader. This is used when the
+     * view is about to be displayed.
+     */
+    public PreloadedTabControl getPreloadedTab(String id) {
+        PreloaderSession s = takeSession(id);
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Showing preload session " + id + "=" + s);
+        return s == null ? null : s.getTabControl();
+    }
+
+    private class PreloaderSession {
+        private final String mId;
+        private final PreloadedTabControl mTabControl;
+
+        private final Runnable mTimeoutTask = new Runnable(){
+            @Override
+            public void run() {
+                if (LOGD_ENABLED) Log.d(LOGTAG, "Preload session timeout " + mId);
+                discardPreload(mId);
+            }};
+
+        public PreloaderSession(String id) {
+            mId = id;
+            mTabControl = new PreloadedTabControl(
+                    new Tab(new PreloadController(mContext), mFactory.createWebView(false)));
+            touch();
+        }
+
+        public void cancelTimeout() {
+            mHandler.removeCallbacks(mTimeoutTask);
+        }
+
+        public void touch() {
+            cancelTimeout();
+            mHandler.postDelayed(mTimeoutTask, PRERENDER_TIMEOUT_MILLIS);
+        }
+
+        public PreloadedTabControl getTabControl() {
+            return mTabControl;
+        }
+
+        public WebView getWebView() {
+            Tab t = mTabControl.getTab();
+            return t == null? null : t.getWebView();
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/ShortcutActivity.java b/src/com/android/browser/ShortcutActivity.java
new file mode 100644
index 0000000..dcc176f
--- /dev/null
+++ b/src/com/android/browser/ShortcutActivity.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2010 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 com.android.browser.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+public class ShortcutActivity extends Activity
+    implements BookmarksPageCallbacks, OnClickListener {
+
+    private BrowserBookmarksPage mBookmarks;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setTitle(R.string.shortcut_bookmark_title);
+        setContentView(R.layout.pick_bookmark);
+        mBookmarks = (BrowserBookmarksPage) getFragmentManager()
+                .findFragmentById(R.id.bookmarks);
+        mBookmarks.setEnableContextMenu(false);
+        mBookmarks.setCallbackListener(this);
+        View cancel = findViewById(R.id.cancel);
+        if (cancel != null) {
+            cancel.setOnClickListener(this);
+        }
+    }
+
+    // BookmarksPageCallbacks
+
+    @Override
+    public boolean onBookmarkSelected(Cursor c, boolean isFolder) {
+        if (isFolder) {
+            return false;
+        }
+        Intent intent = BrowserBookmarksPage.createShortcutIntent(this, c);
+        setResult(RESULT_OK, intent);
+        finish();
+        return true;
+    }
+
+    @Override
+    public boolean onOpenInNewWindow(String... urls) {
+        return false;
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+        case R.id.cancel:
+            finish();
+            break;
+        }
+    }
+}
diff --git a/src/com/android/browser/SnapshotBar.java b/src/com/android/browser/SnapshotBar.java
new file mode 100644
index 0000000..42f9fba
--- /dev/null
+++ b/src/com/android/browser/SnapshotBar.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2011 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.Context;
+import android.graphics.Bitmap;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewPropertyAnimator;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.UI.ComboViews;
+
+import java.text.DateFormat;
+import java.util.Date;
+
+public class SnapshotBar extends LinearLayout implements OnClickListener {
+
+    private static final int MSG_SHOW_TITLE = 1;
+    private static final long DURATION_SHOW_DATE = BaseUi.HIDE_TITLEBAR_DELAY;
+
+    private ImageView mFavicon;
+    private TextView mDate;
+    private TextView mTitle;
+    private View mBookmarks;
+    private TitleBar mTitleBar;
+    private View mTabSwitcher;
+    private View mOverflowMenu;
+    private View mToggleContainer;
+    private boolean mIsAnimating;
+    private ViewPropertyAnimator mTitleAnimator, mDateAnimator;
+    private float mAnimRadius = 20f;
+
+    public SnapshotBar(Context context) {
+        super(context);
+    }
+
+    public SnapshotBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SnapshotBar(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setTitleBar(TitleBar titleBar) {
+        mTitleBar = titleBar;
+        setFavicon(null);
+    }
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_SHOW_TITLE) {
+                mIsAnimating = false;
+                showTitle();
+                mTitleBar.getUi().showTitleBarForDuration();
+            }
+        }
+    };
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        mFavicon = (ImageView) findViewById(R.id.favicon);
+        mDate = (TextView) findViewById(R.id.date);
+        mTitle = (TextView) findViewById(R.id.title);
+        mBookmarks = findViewById(R.id.all_btn);
+        mTabSwitcher = findViewById(R.id.tab_switcher);
+        mOverflowMenu = findViewById(R.id.more);
+        mToggleContainer = findViewById(R.id.toggle_container);
+
+        if (mBookmarks != null) {
+            mBookmarks.setOnClickListener(this);
+        }
+        if (mTabSwitcher != null) {
+            mTabSwitcher.setOnClickListener(this);
+        }
+        if (mOverflowMenu != null) {
+            mOverflowMenu.setOnClickListener(this);
+            boolean showMenu = !ViewConfiguration.get(getContext())
+                    .hasPermanentMenuKey();
+            mOverflowMenu.setVisibility(showMenu ? VISIBLE : GONE);
+        }
+        if (mToggleContainer != null) {
+            mToggleContainer.setOnClickListener(this);
+            resetAnimation();
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        if (mToggleContainer != null) {
+            mAnimRadius = mToggleContainer.getHeight() / 2f;
+        }
+    }
+
+    void resetAnimation() {
+        if (mToggleContainer == null) {
+            // No animation needed/used
+            return;
+        }
+        if (mTitleAnimator != null) {
+            mTitleAnimator.cancel();
+            mTitleAnimator = null;
+        }
+        if (mDateAnimator != null) {
+            mDateAnimator.cancel();
+            mDateAnimator = null;
+        }
+        mIsAnimating = false;
+        mHandler.removeMessages(MSG_SHOW_TITLE);
+        mTitle.setAlpha(1f);
+        mTitle.setTranslationY(0f);
+        mTitle.setRotationX(0f);
+        mDate.setAlpha(0f);
+        mDate.setTranslationY(-mAnimRadius);
+        mDate.setRotationX(90f);
+    }
+
+    private void showDate() {
+        mTitleAnimator = mTitle.animate()
+                .alpha(0f)
+                .translationY(mAnimRadius)
+                .rotationX(-90f);
+        mDateAnimator = mDate.animate()
+                .alpha(1f)
+                .translationY(0f)
+                .rotationX(0f);
+    }
+
+    private void showTitle() {
+        mTitleAnimator = mTitle.animate()
+                .alpha(1f)
+                .translationY(0f)
+                .rotationX(0f);
+        mDateAnimator = mDate.animate()
+                .alpha(0f)
+                .translationY(-mAnimRadius)
+                .rotationX(90f);
+    }
+
+    @Override
+    public void onClick(View v) {
+        if (mBookmarks == v) {
+            mTitleBar.getUiController().bookmarksOrHistoryPicker(ComboViews.Bookmarks);
+        } else if (mTabSwitcher == v) {
+            ((PhoneUi) mTitleBar.getUi()).toggleNavScreen();
+        } else if (mOverflowMenu == v) {
+            NavigationBarBase navBar = mTitleBar.getNavigationBar();
+            if (navBar instanceof NavigationBarPhone) {
+                ((NavigationBarPhone)navBar).showMenu(mOverflowMenu);
+            }
+        } else if (mToggleContainer == v && !mIsAnimating) {
+            mIsAnimating = true;
+            showDate();
+            mTitleBar.getUi().showTitleBar();
+            Message m = mHandler.obtainMessage(MSG_SHOW_TITLE);
+            mHandler.sendMessageDelayed(m, DURATION_SHOW_DATE);
+        }
+    }
+
+    public void onTabDataChanged(Tab tab) {
+        if (!tab.isSnapshot()) return;
+        SnapshotTab snapshot = (SnapshotTab) tab;
+        DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG);
+        mDate.setText(dateFormat.format(new Date(snapshot.getDateCreated())));
+        String title = snapshot.getTitle();
+        if (TextUtils.isEmpty(title)) {
+            title = UrlUtils.stripUrl(snapshot.getUrl());
+        }
+        mTitle.setText(title);
+        setFavicon(tab.getFavicon());
+        resetAnimation();
+    }
+
+    public void setFavicon(Bitmap icon) {
+        if (mFavicon == null) return;
+        mFavicon.setImageDrawable(mTitleBar.getUi().getFaviconDrawable(icon));
+    }
+
+    public boolean isAnimating() {
+        return mIsAnimating;
+    }
+
+}
diff --git a/src/com/android/browser/SnapshotTab.java b/src/com/android/browser/SnapshotTab.java
new file mode 100644
index 0000000..e403dbc
--- /dev/null
+++ b/src/com/android/browser/SnapshotTab.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2011 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.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.util.Log;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.zip.GZIPInputStream;
+
+
+public class SnapshotTab extends Tab {
+
+    private static final String LOGTAG = "SnapshotTab";
+
+    private long mSnapshotId;
+    private LoadData mLoadTask;
+    private WebViewFactory mWebViewFactory;
+    private int mBackgroundColor;
+    private long mDateCreated;
+    private boolean mIsLive;
+    private String mLiveUrl;
+
+    public SnapshotTab(WebViewController wvcontroller, long snapshotId) {
+        super(wvcontroller, null, null);
+        mSnapshotId = snapshotId;
+        mWebViewFactory = mWebViewController.getWebViewFactory();
+        WebView web = mWebViewFactory.createWebView(false);
+        setWebView(web);
+        loadData();
+    }
+
+    @Override
+    void putInForeground() {
+        if (getWebView() == null) {
+            WebView web = mWebViewFactory.createWebView(false);
+            if (mBackgroundColor != 0) {
+                web.setBackgroundColor(mBackgroundColor);
+            }
+            setWebView(web);
+            loadData();
+        }
+        super.putInForeground();
+    }
+
+    @Override
+    void putInBackground() {
+        if (getWebView() == null) return;
+        super.putInBackground();
+    }
+
+    void loadData() {
+        if (mLoadTask == null) {
+            mLoadTask = new LoadData(this, mContext);
+            mLoadTask.execute();
+        }
+    }
+
+    @Override
+    void addChildTab(Tab child) {
+        if (mIsLive) {
+            super.addChildTab(child);
+        } else {
+            throw new IllegalStateException("Snapshot tabs cannot have child tabs!");
+        }
+    }
+
+    @Override
+    public boolean isSnapshot() {
+        return !mIsLive;
+    }
+
+    public long getSnapshotId() {
+        return mSnapshotId;
+    }
+
+    @Override
+    public ContentValues createSnapshotValues() {
+        if (mIsLive) {
+            return super.createSnapshotValues();
+        }
+        return null;
+    }
+
+    @Override
+    public Bundle saveState() {
+        if (mIsLive) {
+            return super.saveState();
+        }
+        return null;
+    }
+
+    public long getDateCreated() {
+        return mDateCreated;
+    }
+
+    public String getLiveUrl() {
+        return mLiveUrl;
+    }
+
+    @Override
+    public void loadUrl(String url, Map<String, String> headers) {
+        if (!mIsLive) {
+            mIsLive = true;
+            getWebView().clearViewState();
+        }
+        super.loadUrl(url, headers);
+    }
+
+    @Override
+    public boolean canGoBack() {
+        return super.canGoBack() || mIsLive;
+    }
+
+    @Override
+    public boolean canGoForward() {
+        return mIsLive && super.canGoForward();
+    }
+
+    @Override
+    public void goBack() {
+        if (super.canGoBack()) {
+            super.goBack();
+        } else {
+            mIsLive = false;
+            getWebView().stopLoading();
+            loadData();
+        }
+    }
+
+    static class LoadData extends AsyncTask<Void, Void, Cursor> {
+
+        static final String[] PROJECTION = new String[] {
+            Snapshots._ID, // 0
+            Snapshots.URL, // 1
+            Snapshots.TITLE, // 2
+            Snapshots.FAVICON, // 3
+            Snapshots.VIEWSTATE, // 4
+            Snapshots.BACKGROUND, // 5
+            Snapshots.DATE_CREATED, // 6
+            Snapshots.VIEWSTATE_PATH, // 7
+        };
+        static final int SNAPSHOT_ID = 0;
+        static final int SNAPSHOT_URL = 1;
+        static final int SNAPSHOT_TITLE = 2;
+        static final int SNAPSHOT_FAVICON = 3;
+        static final int SNAPSHOT_VIEWSTATE = 4;
+        static final int SNAPSHOT_BACKGROUND = 5;
+        static final int SNAPSHOT_DATE_CREATED = 6;
+        static final int SNAPSHOT_VIEWSTATE_PATH = 7;
+
+        private SnapshotTab mTab;
+        private ContentResolver mContentResolver;
+        private Context mContext;
+
+        public LoadData(SnapshotTab t, Context context) {
+            mTab = t;
+            mContentResolver = context.getContentResolver();
+            mContext = context;
+        }
+
+        @Override
+        protected Cursor doInBackground(Void... params) {
+            long id = mTab.mSnapshotId;
+            Uri uri = ContentUris.withAppendedId(Snapshots.CONTENT_URI, id);
+            return mContentResolver.query(uri, PROJECTION, null, null, null);
+        }
+
+        private InputStream getInputStream(Cursor c) throws FileNotFoundException {
+            byte[] data = c.getBlob(SNAPSHOT_VIEWSTATE);
+            ByteArrayInputStream bis = new ByteArrayInputStream(data);
+            return bis;
+        }
+
+        @Override
+        protected void onPostExecute(Cursor result) {
+            try {
+                if (result.moveToFirst()) {
+                    mTab.mCurrentState.mTitle = result.getString(SNAPSHOT_TITLE);
+                    mTab.mCurrentState.mUrl = result.getString(SNAPSHOT_URL);
+                    mTab.mLiveUrl = result.getString(SNAPSHOT_URL);
+                    byte[] favicon = result.getBlob(SNAPSHOT_FAVICON);
+                    if (favicon != null) {
+                        mTab.mCurrentState.mFavicon = BitmapFactory
+                                .decodeByteArray(favicon, 0, favicon.length);
+                    }
+                    WebView web = mTab.getWebView();
+                    if (web != null) {
+                        String path = result.getString(SNAPSHOT_VIEWSTATE_PATH);
+                        if (!TextUtils.isEmpty(path)) {
+                             web.loadViewState(path);
+                        } else {
+                            InputStream ins = getInputStream(result);
+                            GZIPInputStream stream = new GZIPInputStream(ins);
+                            web.loadViewState(stream);
+                        }
+                    }
+                    mTab.mBackgroundColor = result.getInt(SNAPSHOT_BACKGROUND);
+                    mTab.mDateCreated = result.getLong(SNAPSHOT_DATE_CREATED);
+                    mTab.mWebViewController.onPageFinished(mTab);
+                }
+            } catch (Exception e) {
+                Log.w(LOGTAG, "Failed to load view state, closing tab", e);
+                mTab.mWebViewController.closeTab(mTab);
+            } finally {
+                if (result != null) {
+                    result.close();
+                }
+                mTab.mLoadTask = null;
+            }
+        }
+
+    }
+
+    @Override
+    protected void persistThumbnail() {
+        if (mIsLive) {
+            super.persistThumbnail();
+        }
+    }
+}
diff --git a/src/com/android/browser/SuggestionsAdapter.java b/src/com/android/browser/SuggestionsAdapter.java
new file mode 100644
index 0000000..41d2b74
--- /dev/null
+++ b/src/com/android/browser/SuggestionsAdapter.java
@@ -0,0 +1,572 @@
+/*
+ * Copyright (C) 2010 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.SearchManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.AsyncTask;
+
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.provider.BrowserProvider2.OmniboxSuggestions;
+import com.android.browser.search.SearchEngine;
+
+import android.text.Html;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.Filter;
+import android.widget.Filterable;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * adapter to wrap multiple cursors for url/search completions
+ */
+public class SuggestionsAdapter extends BaseAdapter implements Filterable,
+        OnClickListener {
+
+    public static final int TYPE_BOOKMARK = 0;
+    public static final int TYPE_HISTORY = 1;
+    public static final int TYPE_SUGGEST_URL = 2;
+    public static final int TYPE_SEARCH = 3;
+    public static final int TYPE_SUGGEST = 4;
+
+    private static final String[] COMBINED_PROJECTION = {
+            OmniboxSuggestions._ID,
+            OmniboxSuggestions.TITLE,
+            OmniboxSuggestions.URL,
+            OmniboxSuggestions.IS_BOOKMARK
+            };
+
+    private static final String COMBINED_SELECTION =
+            "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ? OR title LIKE ?)";
+
+    final Context mContext;
+    final Filter mFilter;
+    SuggestionResults mMixedResults;
+    List<SuggestItem> mSuggestResults, mFilterResults;
+    List<CursorSource> mSources;
+    boolean mLandscapeMode;
+    final CompletionListener mListener;
+    final int mLinesPortrait;
+    final int mLinesLandscape;
+    final Object mResultsLock = new Object();
+    boolean mIncognitoMode;
+    BrowserSettings mSettings;
+
+    interface CompletionListener {
+
+        public void onSearch(String txt);
+
+        public void onSelect(String txt, int type, String extraData);
+
+    }
+
+    public SuggestionsAdapter(Context ctx, CompletionListener listener) {
+        mContext = ctx;
+        mSettings = BrowserSettings.getInstance();
+        mListener = listener;
+        mLinesPortrait = mContext.getResources().
+                getInteger(R.integer.max_suggest_lines_portrait);
+        mLinesLandscape = mContext.getResources().
+                getInteger(R.integer.max_suggest_lines_landscape);
+
+        mFilter = new SuggestFilter();
+        addSource(new CombinedCursor());
+    }
+
+    public void setLandscapeMode(boolean mode) {
+        mLandscapeMode = mode;
+        notifyDataSetChanged();
+    }
+
+    public void addSource(CursorSource c) {
+        if (mSources == null) {
+            mSources = new ArrayList<CursorSource>(5);
+        }
+        mSources.add(c);
+    }
+
+    @Override
+    public void onClick(View v) {
+        SuggestItem item = (SuggestItem) ((View) v.getParent()).getTag();
+
+        if (R.id.icon2 == v.getId()) {
+            // replace input field text with suggestion text
+            mListener.onSearch(getSuggestionUrl(item));
+        } else {
+            mListener.onSelect(getSuggestionUrl(item), item.type, item.extra);
+        }
+    }
+
+    @Override
+    public Filter getFilter() {
+        return mFilter;
+    }
+
+    @Override
+    public int getCount() {
+        return (mMixedResults == null) ? 0 : mMixedResults.getLineCount();
+    }
+
+    @Override
+    public SuggestItem getItem(int position) {
+        if (mMixedResults == null) {
+            return null;
+        }
+        return mMixedResults.items.get(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        final LayoutInflater inflater = LayoutInflater.from(mContext);
+        View view = convertView;
+        if (view == null) {
+            view = inflater.inflate(R.layout.suggestion_item, parent, false);
+        }
+        bindView(view, getItem(position));
+        return view;
+    }
+
+    private void bindView(View view, SuggestItem item) {
+        // store item for click handling
+        view.setTag(item);
+        TextView tv1 = (TextView) view.findViewById(android.R.id.text1);
+        TextView tv2 = (TextView) view.findViewById(android.R.id.text2);
+        ImageView ic1 = (ImageView) view.findViewById(R.id.icon1);
+        View ic2 = view.findViewById(R.id.icon2);
+        View div = view.findViewById(R.id.divider);
+        tv1.setText(Html.fromHtml(item.title));
+        if (TextUtils.isEmpty(item.url)) {
+            tv2.setVisibility(View.GONE);
+            tv1.setMaxLines(2);
+        } else {
+            tv2.setVisibility(View.VISIBLE);
+            tv2.setText(item.url);
+            tv1.setMaxLines(1);
+        }
+        int id = -1;
+        switch (item.type) {
+            case TYPE_SUGGEST:
+            case TYPE_SEARCH:
+                id = R.drawable.ic_search_category_suggest;
+                break;
+            case TYPE_BOOKMARK:
+                id = R.drawable.ic_search_category_bookmark;
+                break;
+            case TYPE_HISTORY:
+                id = R.drawable.ic_search_category_history;
+                break;
+            case TYPE_SUGGEST_URL:
+                id = R.drawable.ic_search_category_browser;
+                break;
+            default:
+                id = -1;
+        }
+        if (id != -1) {
+            ic1.setImageDrawable(mContext.getResources().getDrawable(id));
+        }
+        ic2.setVisibility(((TYPE_SUGGEST == item.type)
+                || (TYPE_SEARCH == item.type))
+                ? View.VISIBLE : View.GONE);
+        div.setVisibility(ic2.getVisibility());
+        ic2.setOnClickListener(this);
+        view.findViewById(R.id.suggestion).setOnClickListener(this);
+    }
+
+    class SlowFilterTask extends AsyncTask<CharSequence, Void, List<SuggestItem>> {
+
+        @Override
+        protected List<SuggestItem> doInBackground(CharSequence... params) {
+            SuggestCursor cursor = new SuggestCursor();
+            cursor.runQuery(params[0]);
+            List<SuggestItem> results = new ArrayList<SuggestItem>();
+            int count = cursor.getCount();
+            for (int i = 0; i < count; i++) {
+                results.add(cursor.getItem());
+                cursor.moveToNext();
+            }
+            cursor.close();
+            return results;
+        }
+
+        @Override
+        protected void onPostExecute(List<SuggestItem> items) {
+            mSuggestResults = items;
+            mMixedResults = buildSuggestionResults();
+            notifyDataSetChanged();
+        }
+    }
+
+    SuggestionResults buildSuggestionResults() {
+        SuggestionResults mixed = new SuggestionResults();
+        List<SuggestItem> filter, suggest;
+        synchronized (mResultsLock) {
+            filter = mFilterResults;
+            suggest = mSuggestResults;
+        }
+        if (filter != null) {
+            for (SuggestItem item : filter) {
+                mixed.addResult(item);
+            }
+        }
+        if (suggest != null) {
+            for (SuggestItem item : suggest) {
+                mixed.addResult(item);
+            }
+        }
+        return mixed;
+    }
+
+    class SuggestFilter extends Filter {
+
+        @Override
+        public CharSequence convertResultToString(Object item) {
+            if (item == null) {
+                return "";
+            }
+            SuggestItem sitem = (SuggestItem) item;
+            if (sitem.title != null) {
+                return sitem.title;
+            } else {
+                return sitem.url;
+            }
+        }
+
+        void startSuggestionsAsync(final CharSequence constraint) {
+            if (!mIncognitoMode) {
+                new SlowFilterTask().execute(constraint);
+            }
+        }
+
+        private boolean shouldProcessEmptyQuery() {
+            final SearchEngine searchEngine = mSettings.getSearchEngine();
+            return searchEngine.wantsEmptyQuery();
+        }
+
+        @Override
+        protected FilterResults performFiltering(CharSequence constraint) {
+            FilterResults res = new FilterResults();
+            if (TextUtils.isEmpty(constraint) && !shouldProcessEmptyQuery()) {
+                res.count = 0;
+                res.values = null;
+                return res;
+            }
+            startSuggestionsAsync(constraint);
+            List<SuggestItem> filterResults = new ArrayList<SuggestItem>();
+            if (constraint != null) {
+                for (CursorSource sc : mSources) {
+                    sc.runQuery(constraint);
+                }
+                mixResults(filterResults);
+            }
+            synchronized (mResultsLock) {
+                mFilterResults = filterResults;
+            }
+            SuggestionResults mixed = buildSuggestionResults();
+            res.count = mixed.getLineCount();
+            res.values = mixed;
+            return res;
+        }
+
+        void mixResults(List<SuggestItem> results) {
+            int maxLines = getMaxLines();
+            for (int i = 0; i < mSources.size(); i++) {
+                CursorSource s = mSources.get(i);
+                int n = Math.min(s.getCount(), maxLines);
+                maxLines -= n;
+                boolean more = false;
+                for (int j = 0; j < n; j++) {
+                    results.add(s.getItem());
+                    more = s.moveToNext();
+                }
+            }
+        }
+
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults fresults) {
+            if (fresults.values instanceof SuggestionResults) {
+                mMixedResults = (SuggestionResults) fresults.values;
+                notifyDataSetChanged();
+            }
+        }
+    }
+
+    private int getMaxLines() {
+        int maxLines = mLandscapeMode ? mLinesLandscape : mLinesPortrait;
+        maxLines = (int) Math.ceil(maxLines / 2.0);
+        return maxLines;
+    }
+
+    /**
+     * sorted list of results of a suggestion query
+     *
+     */
+    class SuggestionResults {
+
+        ArrayList<SuggestItem> items;
+        // count per type
+        int[] counts;
+
+        SuggestionResults() {
+            items = new ArrayList<SuggestItem>(24);
+            // n of types:
+            counts = new int[5];
+        }
+
+        int getTypeCount(int type) {
+            return counts[type];
+        }
+
+        void addResult(SuggestItem item) {
+            int ix = 0;
+            while ((ix < items.size()) && (item.type >= items.get(ix).type))
+                ix++;
+            items.add(ix, item);
+            counts[item.type]++;
+        }
+
+        int getLineCount() {
+            return Math.min((mLandscapeMode ? mLinesLandscape : mLinesPortrait), items.size());
+        }
+
+        @Override
+        public String toString() {
+            if (items == null) return null;
+            if (items.size() == 0) return "[]";
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < items.size(); i++) {
+                SuggestItem item = items.get(i);
+                sb.append(item.type + ": " + item.title);
+                if (i < items.size() - 1) {
+                    sb.append(", ");
+                }
+            }
+            return sb.toString();
+        }
+    }
+
+    /**
+     * data object to hold suggestion values
+     */
+    public class SuggestItem {
+        public String title;
+        public String url;
+        public int type;
+        public String extra;
+
+        public SuggestItem(String text, String u, int t) {
+            title = text;
+            url = u;
+            type = t;
+        }
+
+    }
+
+    abstract class CursorSource {
+
+        Cursor mCursor;
+
+        boolean moveToNext() {
+            return mCursor.moveToNext();
+        }
+
+        public abstract void runQuery(CharSequence constraint);
+
+        public abstract SuggestItem getItem();
+
+        public int getCount() {
+            return (mCursor != null) ? mCursor.getCount() : 0;
+        }
+
+        public void close() {
+            if (mCursor != null) {
+                mCursor.close();
+            }
+        }
+    }
+
+    /**
+     * combined bookmark & history source
+     */
+    class CombinedCursor extends CursorSource {
+
+        @Override
+        public SuggestItem getItem() {
+            if ((mCursor != null) && (!mCursor.isAfterLast())) {
+                String title = mCursor.getString(1);
+                String url = mCursor.getString(2);
+                boolean isBookmark = (mCursor.getInt(3) == 1);
+                return new SuggestItem(getTitle(title, url), getUrl(title, url),
+                        isBookmark ? TYPE_BOOKMARK : TYPE_HISTORY);
+            }
+            return null;
+        }
+
+        @Override
+        public void runQuery(CharSequence constraint) {
+            // constraint != null
+            if (mCursor != null) {
+                mCursor.close();
+            }
+            String like = constraint + "%";
+            String[] args = null;
+            String selection = null;
+            if (like.startsWith("http") || like.startsWith("file")) {
+                args = new String[1];
+                args[0] = like;
+                selection = "url LIKE ?";
+            } else {
+                args = new String[5];
+                args[0] = "http://" + like;
+                args[1] = "http://www." + like;
+                args[2] = "https://" + like;
+                args[3] = "https://www." + like;
+                // To match against titles.
+                args[4] = like;
+                selection = COMBINED_SELECTION;
+            }
+            Uri.Builder ub = OmniboxSuggestions.CONTENT_URI.buildUpon();
+            ub.appendQueryParameter(BrowserContract.PARAM_LIMIT,
+                    Integer.toString(Math.max(mLinesLandscape, mLinesPortrait)));
+            mCursor =
+                    mContext.getContentResolver().query(ub.build(), COMBINED_PROJECTION,
+                            selection, (constraint != null) ? args : null, null);
+            if (mCursor != null) {
+                mCursor.moveToFirst();
+            }
+        }
+
+        /**
+         * Provides the title (text line 1) for a browser suggestion, which should be the
+         * webpage title. If the webpage title is empty, returns the stripped url instead.
+         *
+         * @return the title string to use
+         */
+        private String getTitle(String title, String url) {
+            if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
+                title = UrlUtils.stripUrl(url);
+            }
+            return title;
+        }
+
+        /**
+         * Provides the subtitle (text line 2) for a browser suggestion, which should be the
+         * webpage url. If the webpage title is empty, then the url should go in the title
+         * instead, and the subtitle should be empty, so this would return null.
+         *
+         * @return the subtitle string to use, or null if none
+         */
+        private String getUrl(String title, String url) {
+            if (TextUtils.isEmpty(title)
+                    || TextUtils.getTrimmedLength(title) == 0
+                    || title.equals(url)) {
+                return null;
+            } else {
+                return UrlUtils.stripUrl(url);
+            }
+        }
+    }
+
+    class SuggestCursor extends CursorSource {
+
+        @Override
+        public SuggestItem getItem() {
+            if (mCursor != null) {
+                String title = mCursor.getString(
+                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1));
+                String text2 = mCursor.getString(
+                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2));
+                String url = mCursor.getString(
+                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL));
+                String uri = mCursor.getString(
+                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_DATA));
+                int type = (TextUtils.isEmpty(url)) ? TYPE_SUGGEST : TYPE_SUGGEST_URL;
+                SuggestItem item = new SuggestItem(title, url, type);
+                item.extra = mCursor.getString(
+                        mCursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA));
+                return item;
+            }
+            return null;
+        }
+
+        @Override
+        public void runQuery(CharSequence constraint) {
+            if (mCursor != null) {
+                mCursor.close();
+            }
+            SearchEngine searchEngine = mSettings.getSearchEngine();
+            if (!TextUtils.isEmpty(constraint)) {
+                if (searchEngine != null && searchEngine.supportsSuggestions()) {
+                    mCursor = searchEngine.getSuggestions(mContext, constraint.toString());
+                    if (mCursor != null) {
+                        mCursor.moveToFirst();
+                    }
+                }
+            } else {
+                if (searchEngine.wantsEmptyQuery()) {
+                    mCursor = searchEngine.getSuggestions(mContext, "");
+                }
+                mCursor = null;
+            }
+        }
+
+    }
+
+    public void clearCache() {
+        mFilterResults = null;
+        mSuggestResults = null;
+        notifyDataSetInvalidated();
+    }
+
+    public void setIncognitoMode(boolean incognito) {
+        mIncognitoMode = incognito;
+        clearCache();
+    }
+
+    static String getSuggestionTitle(SuggestItem item) {
+        // There must be a better way to strip HTML from things.
+        // This method is used in multiple places. It is also more
+        // expensive than a standard html escaper.
+        return (item.title != null) ? Html.fromHtml(item.title).toString() : null;
+    }
+
+    static String getSuggestionUrl(SuggestItem item) {
+        final String title = SuggestionsAdapter.getSuggestionTitle(item);
+
+        if (TextUtils.isEmpty(item.url)) {
+            return title;
+        }
+
+        return item.url;
+    }
+}
diff --git a/src/com/android/browser/SystemAllowGeolocationOrigins.java b/src/com/android/browser/SystemAllowGeolocationOrigins.java
new file mode 100644
index 0000000..f4b5835
--- /dev/null
+++ b/src/com/android/browser/SystemAllowGeolocationOrigins.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2010 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.Context;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.webkit.ValueCallback;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.codeaurora.swe.GeolocationPermissions;
+
+/**
+ * Manages the interaction between the secure system setting for default geolocation
+ * permissions and the browser.
+ */
+class SystemAllowGeolocationOrigins {
+
+    // Preference key for the value of the system setting last read by the browser
+    private final static String LAST_READ_ALLOW_GEOLOCATION_ORIGINS =
+            "last_read_allow_geolocation_origins";
+
+    // The application context
+    private final Context mContext;
+
+    // The observer used to listen to the system setting.
+    private final SettingObserver mSettingObserver;
+
+    public SystemAllowGeolocationOrigins(Context context) {
+        mContext = context.getApplicationContext();
+        mSettingObserver = new SettingObserver();
+    }
+
+    /**
+     * Checks whether the setting has changed and installs an observer to listen for
+     * future changes. Must be called on the application main thread.
+     */
+    public void start() {
+        // Register to receive notifications when the system settings change.
+        Uri uri = Settings.Secure.getUriFor(Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS);
+        mContext.getContentResolver().registerContentObserver(uri, false, mSettingObserver);
+
+        // Read and apply the setting if needed.
+        maybeApplySettingAsync();
+    }
+
+    /**
+     * Stops the manager.
+     */
+    public void stop() {
+        mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
+    }
+
+    void maybeApplySettingAsync() {
+        BackgroundHandler.execute(mMaybeApplySetting);
+    }
+
+    /**
+     * Checks to see if the system setting has changed and if so,
+     * updates the Geolocation permissions accordingly.
+     */
+    private Runnable mMaybeApplySetting = new Runnable() {
+
+        @Override
+        public void run() {
+         // Get the new value
+            String newSetting = getSystemSetting();
+
+            // Get the last read value
+            SharedPreferences preferences = BrowserSettings.getInstance()
+                    .getPreferences();
+            String lastReadSetting =
+                    preferences.getString(LAST_READ_ALLOW_GEOLOCATION_ORIGINS, "");
+
+            // If the new value is the same as the last one we read, we're done.
+            if (TextUtils.equals(lastReadSetting, newSetting)) {
+                return;
+            }
+
+            // Save the new value as the last read value
+            preferences.edit()
+                    .putString(LAST_READ_ALLOW_GEOLOCATION_ORIGINS, newSetting)
+                    .apply();
+
+            Set<String> oldOrigins = parseAllowGeolocationOrigins(lastReadSetting);
+            Set<String> newOrigins = parseAllowGeolocationOrigins(newSetting);
+            Set<String> addedOrigins = setMinus(newOrigins, oldOrigins);
+            Set<String> removedOrigins = setMinus(oldOrigins, newOrigins);
+
+            // Remove the origins in the last read value
+            removeOrigins(removedOrigins);
+
+            // Add the origins in the new value
+            addOrigins(addedOrigins);
+        }
+    };
+
+    /**
+     * Parses the value of the default geolocation permissions setting.
+     *
+     * @param setting A space-separated list of origins.
+     * @return A mutable set of origins.
+     */
+    private static HashSet<String> parseAllowGeolocationOrigins(String setting) {
+        HashSet<String> origins = new HashSet<String>();
+        if (!TextUtils.isEmpty(setting)) {
+            for (String origin : setting.split("\\s+")) {
+                if (!TextUtils.isEmpty(origin)) {
+                    origins.add(origin);
+                }
+            }
+        }
+        return origins;
+    }
+
+    /**
+     * Gets the difference between two sets. Does not modify any of the arguments.
+     *
+     * @return A set containing all elements in {@code x} that are not in {@code y}.
+     */
+    private <A> Set<A> setMinus(Set<A> x, Set<A> y) {
+        HashSet<A> z = new HashSet<A>(x.size());
+        for (A a : x) {
+            if (!y.contains(a)) {
+                z.add(a);
+            }
+        }
+        return z;
+    }
+
+    /**
+     * Gets the current system setting for default allowed geolocation origins.
+     *
+     * @return The default allowed origins. Returns {@code ""} if not set.
+     */
+    private String getSystemSetting() {
+        String value = Settings.Secure.getString(mContext.getContentResolver(),
+                Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS);
+        return value == null ? "" : value;
+    }
+
+    /**
+     * Adds geolocation permissions for the given origins.
+     */
+    private void addOrigins(Set<String> origins) {
+        for (String origin : origins) {
+            GeolocationPermissions.getInstance().allow(origin);
+        }
+    }
+
+    /**
+     * Removes geolocation permissions for the given origins, if they are allowed.
+     * If they are denied or not set, nothing is done.
+     */
+    private void removeOrigins(Set<String> origins) {
+        for (final String origin : origins) {
+            GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
+                public void onReceiveValue(Boolean value) {
+                    if (value != null && value.booleanValue()) {
+                        GeolocationPermissions.getInstance().clear(origin);
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Listens for changes to the system setting.
+     */
+    private class SettingObserver extends ContentObserver {
+
+        SettingObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            maybeApplySettingAsync();
+        }
+    }
+
+}
diff --git a/src/com/android/browser/Tab.java b/src/com/android/browser/Tab.java
new file mode 100644
index 0000000..6d36d9c
--- /dev/null
+++ b/src/com/android/browser/Tab.java
@@ -0,0 +1,2108 @@
+/*
+ * Copyright (C) 2009 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.app.AlertDialog;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.CompressFormat;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Picture;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.net.Uri;
+import android.net.http.SslError;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewStub;
+import android.view.View.OnClickListener;
+import android.webkit.ConsoleMessage;
+import android.webkit.GeolocationPermissions;
+import android.webkit.URLUtil;
+import android.webkit.WebResourceResponse;
+import android.webkit.WebStorage;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import android.webkit.ValueCallback;
+import android.widget.CheckBox;
+import android.widget.Toast;
+import android.widget.FrameLayout;
+import android.widget.Button;
+
+import com.android.browser.R;
+import com.android.browser.TabControl.OnThumbnailUpdatedListener;
+import com.android.browser.homepages.HomeProvider;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.provider.MyNavigationProvider;
+import com.android.browser.provider.SnapshotProvider.Snapshots;
+
+import org.codeaurora.swe.BrowserDownloadListener;
+import org.codeaurora.swe.ClientCertRequestHandler;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import org.codeaurora.swe.WebBackForwardList;
+import org.codeaurora.swe.WebBackForwardListClient;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebHistoryItem;
+import org.codeaurora.swe.WebView;
+import org.codeaurora.swe.WebView.PictureListener;
+import org.codeaurora.swe.WebViewClient;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.UUID;
+import java.util.Vector;
+import java.util.regex.Pattern;
+import java.sql.Timestamp;
+import java.util.Date;
+
+/**
+ * Class for maintaining Tabs with a main WebView and a subwindow.
+ */
+class Tab implements PictureListener {
+
+    // Log Tag
+    private static final String LOGTAG = "Tab";
+    private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+    // Special case the logtag for messages for the Console to make it easier to
+    // filter them and match the logtag used for these messages in older versions
+    // of the browser.
+    private static final String CONSOLE_LOGTAG = "browser";
+
+    private static final int MSG_CAPTURE = 42;
+    private static final int CAPTURE_DELAY = 1000;
+    private static final int INITIAL_PROGRESS = 5;
+
+    private static Bitmap sDefaultFavicon;
+    protected boolean hasCrashed = false;
+
+    private static Paint sAlphaPaint = new Paint();
+    static {
+        sAlphaPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
+        sAlphaPaint.setColor(Color.TRANSPARENT);
+    }
+
+    public enum SecurityState {
+        // The page's main resource does not use SSL. Note that we use this
+        // state irrespective of the SSL authentication state of sub-resources.
+        SECURITY_STATE_NOT_SECURE,
+        // The page's main resource uses SSL and the certificate is good. The
+        // same is true of all sub-resources.
+        SECURITY_STATE_SECURE,
+        // The page's main resource uses SSL and the certificate is good, but
+        // some sub-resources either do not use SSL or have problems with their
+        // certificates.
+        SECURITY_STATE_MIXED,
+        // The page's main resource uses SSL but there is a problem with its
+        // certificate.
+        SECURITY_STATE_BAD_CERTIFICATE,
+    }
+
+    Context mContext;
+    protected WebViewController mWebViewController;
+
+    // The tab ID
+    private long mId = -1;
+
+    // The Geolocation permissions prompt
+    private GeolocationPermissionsPrompt mGeolocationPermissionsPrompt;
+    // Main WebView wrapper
+    private View mContainer;
+    // Main WebView
+    private WebView mMainView;
+    // Subwindow container
+    private View mSubViewContainer;
+    // Subwindow WebView
+    private WebView mSubView;
+    // Saved bundle for when we are running low on memory. It contains the
+    // information needed to restore the WebView if the user goes back to the
+    // tab.
+    private Bundle mSavedState;
+    // Parent Tab. This is the Tab that created this Tab, or null if the Tab was
+    // created by the UI
+    private Tab mParent;
+    // Tab that constructed by this Tab. This is used when this Tab is
+    // destroyed, it clears all mParentTab values in the children.
+    private Vector<Tab> mChildren;
+    // If true, the tab is in the foreground of the current activity.
+    private boolean mInForeground;
+    // If true, the tab is in page loading state (after onPageStarted,
+    // before onPageFinsihed)
+    private boolean mInPageLoad;
+    private boolean mDisableOverrideUrlLoading;
+    // The last reported progress of the current page
+    private int mPageLoadProgress;
+    // The time the load started, used to find load page time
+    private long mLoadStartTime;
+    // Application identifier used to find tabs that another application wants
+    // to reuse.
+    private String mAppId;
+    // flag to indicate if tab should be closed on back
+    private boolean mCloseOnBack;
+    // Keep the original url around to avoid killing the old WebView if the url
+    // has not changed.
+    // Error console for the tab
+    private ErrorConsoleView mErrorConsole;
+    // The listener that gets invoked when a download is started from the
+    // mMainView
+    private final BrowserDownloadListener mDownloadListener;
+    // Listener used to know when we move forward or back in the history list.
+    private final WebBackForwardListClient mWebBackForwardListClient;
+    private DataController mDataController;
+    // State of the auto-login request.
+    private DeviceAccountLogin mDeviceAccountLogin;
+
+    // AsyncTask for downloading touch icons
+    DownloadTouchIcon mTouchIconLoader;
+
+    private BrowserSettings mSettings;
+    private int mCaptureWidth;
+    private int mCaptureHeight;
+    private Bitmap mCapture;
+    private Handler mHandler;
+    private boolean mUpdateThumbnail;
+    private Timestamp timestamp;
+
+    /**
+     * See {@link #clearBackStackWhenItemAdded(String)}.
+     */
+    private Pattern mClearHistoryUrlPattern;
+
+    private static synchronized Bitmap getDefaultFavicon(Context context) {
+        if (sDefaultFavicon == null) {
+            sDefaultFavicon = BitmapFactory.decodeResource(
+                    context.getResources(), R.drawable.app_web_browser_sm);
+        }
+        return sDefaultFavicon;
+    }
+
+    // All the state needed for a page
+    protected static class PageState {
+        String mUrl;
+        String mOriginalUrl;
+        String mTitle;
+        SecurityState mSecurityState;
+        // This is non-null only when mSecurityState is SECURITY_STATE_BAD_CERTIFICATE.
+        SslError mSslCertificateError;
+        Bitmap mFavicon;
+        boolean mIsBookmarkedSite;
+        boolean mIncognito;
+
+        PageState(Context c, boolean incognito) {
+            mIncognito = incognito;
+            if (mIncognito) {
+                mOriginalUrl = mUrl = "browser:incognito";
+                mTitle = c.getString(R.string.new_incognito_tab);
+            } else {
+                mOriginalUrl = mUrl = "";
+                mTitle = c.getString(R.string.new_tab);
+            }
+            mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+        }
+
+        PageState(Context c, boolean incognito, String url, Bitmap favicon) {
+            mIncognito = incognito;
+            mOriginalUrl = mUrl = url;
+            if (URLUtil.isHttpsUrl(url)) {
+                mSecurityState = SecurityState.SECURITY_STATE_SECURE;
+            } else {
+                mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+            }
+            mFavicon = favicon;
+        }
+
+    }
+
+    // The current/loading page's state
+    protected PageState mCurrentState;
+
+    // Used for saving and restoring each Tab
+    static final String ID = "ID";
+    static final String CURRURL = "currentUrl";
+    static final String CURRTITLE = "currentTitle";
+    static final String PARENTTAB = "parentTab";
+    static final String APPID = "appid";
+    static final String INCOGNITO = "privateBrowsingEnabled";
+    static final String USERAGENT = "useragent";
+    static final String CLOSEFLAG = "closeOnBack";
+
+    // Container class for the next error dialog that needs to be displayed
+    private class ErrorDialog {
+        public final int mTitle;
+        public final String mDescription;
+        public final int mError;
+        ErrorDialog(int title, String desc, int error) {
+            mTitle = title;
+            mDescription = desc;
+            mError = error;
+        }
+    }
+
+    private void processNextError() {
+        if (mQueuedErrors == null) {
+            return;
+        }
+        // The first one is currently displayed so just remove it.
+        mQueuedErrors.removeFirst();
+        if (mQueuedErrors.size() == 0) {
+            mQueuedErrors = null;
+            return;
+        }
+        showError(mQueuedErrors.getFirst());
+    }
+
+    private DialogInterface.OnDismissListener mDialogListener =
+            new DialogInterface.OnDismissListener() {
+                public void onDismiss(DialogInterface d) {
+                    processNextError();
+                }
+            };
+    private LinkedList<ErrorDialog> mQueuedErrors;
+
+    private void queueError(int err, String desc) {
+        if (mQueuedErrors == null) {
+            mQueuedErrors = new LinkedList<ErrorDialog>();
+        }
+        for (ErrorDialog d : mQueuedErrors) {
+            if (d.mError == err) {
+                // Already saw a similar error, ignore the new one.
+                return;
+            }
+        }
+        ErrorDialog errDialog = new ErrorDialog(
+                err == WebViewClient.ERROR_FILE_NOT_FOUND ?
+                R.string.browserFrameFileErrorLabel :
+                R.string.browserFrameNetworkErrorLabel,
+                desc, err);
+        mQueuedErrors.addLast(errDialog);
+
+        // Show the dialog now if the queue was empty and it is in foreground
+        if (mQueuedErrors.size() == 1 && mInForeground) {
+            showError(errDialog);
+        }
+    }
+
+    private void showError(ErrorDialog errDialog) {
+        if (mInForeground) {
+            AlertDialog d = new AlertDialog.Builder(mContext)
+                    .setTitle(errDialog.mTitle)
+                    .setMessage(errDialog.mDescription)
+                    .setPositiveButton(R.string.ok, null)
+                    .create();
+            d.setOnDismissListener(mDialogListener);
+            d.show();
+        }
+    }
+
+    protected void replaceCrashView(View view, View container) {
+        if (hasCrashed && (view == mMainView)) {
+            final FrameLayout wrapper = (FrameLayout) container.findViewById(R.id.webview_wrapper);
+            wrapper.removeAllViewsInLayout();
+            wrapper.addView(view);
+            hasCrashed = false;
+        }
+    }
+
+    protected void showCrashView() {
+        if (hasCrashed) {
+            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+            final View crashLayout = inflater.inflate(R.layout.browser_tab_crash, null);
+            final FrameLayout wrapper =
+                    (FrameLayout) mContainer.findViewById(R.id.webview_wrapper);
+            wrapper.removeAllViewsInLayout();
+            wrapper.addView(crashLayout);
+            mContainer.requestFocus();
+            Button reloadBtn = (Button) crashLayout.findViewById(R.id.browser_crash_reload_btn);
+            reloadBtn.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View arg0) {
+                    replaceCrashView(mMainView, mContainer);
+                    mMainView.reload();
+                }
+            });
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // WebViewClient implementation for the main WebView
+    // -------------------------------------------------------------------------
+
+    private final WebViewClient mWebViewClient = new WebViewClient() {
+        private Message mDontResend;
+        private Message mResend;
+
+        private boolean providersDiffer(String url, String otherUrl) {
+            Uri uri1 = Uri.parse(url);
+            Uri uri2 = Uri.parse(otherUrl);
+            return !uri1.getEncodedAuthority().equals(uri2.getEncodedAuthority());
+        }
+
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            mInPageLoad = true;
+            mUpdateThumbnail = true;
+            mPageLoadProgress = INITIAL_PROGRESS;
+            mCurrentState = new PageState(mContext,
+                    view.isPrivateBrowsingEnabled(), url, favicon);
+            mLoadStartTime = SystemClock.uptimeMillis();
+
+            // If we start a touch icon load and then load a new page, we don't
+            // want to cancel the current touch icon loader. But, we do want to
+            // create a new one when the touch icon url is known.
+            if (mTouchIconLoader != null) {
+                mTouchIconLoader.mTab = null;
+                mTouchIconLoader = null;
+            }
+
+            // reset the error console
+            if (mErrorConsole != null) {
+                mErrorConsole.clearErrorMessages();
+                if (mWebViewController.shouldShowErrorConsole()) {
+                    mErrorConsole.showConsole(ErrorConsoleView.SHOW_NONE);
+                }
+            }
+
+            // Cancel the auto-login process.
+            if (mDeviceAccountLogin != null) {
+                mDeviceAccountLogin.cancel();
+                mDeviceAccountLogin = null;
+                mWebViewController.hideAutoLogin(Tab.this);
+            }
+
+            // finally update the UI in the activity if it is in the foreground
+            mWebViewController.onPageStarted(Tab.this, view, favicon);
+
+            updateBookmarkedStatus();
+        }
+
+        @Override
+        public void onPageFinished(WebView view, String url) {
+            mDisableOverrideUrlLoading = false;
+            if (!isPrivateBrowsingEnabled()) {
+                LogTag.logPageFinishedLoading(
+                        url, SystemClock.uptimeMillis() - mLoadStartTime);
+            }
+            syncCurrentState(view, url);
+            mWebViewController.onPageFinished(Tab.this);
+        }
+
+        // return true if want to hijack the url to let another app to handle it
+        @Override
+        public boolean shouldOverrideUrlLoading(WebView view, String url) {
+            if (!mDisableOverrideUrlLoading && mInForeground) {
+                return mWebViewController.shouldOverrideUrlLoading(Tab.this,
+                        view, url);
+            } else {
+                return false;
+            }
+        }
+
+        /**
+         * Updates the security state. This method is called when we discover
+         * another resource to be loaded for this page (for example,
+         * javascript). While we update the security state, we do not update
+         * the lock icon until we are done loading, as it is slightly more
+         * secure this way.
+         */
+        @Override
+        public void onLoadResource(WebView view, String url) {
+            if (url != null && url.length() > 0) {
+                // It is only if the page claims to be secure that we may have
+                // to update the security state:
+                if (mCurrentState.mSecurityState == SecurityState.SECURITY_STATE_SECURE) {
+                    // If NOT a 'safe' url, change the state to mixed content!
+                    if (!(URLUtil.isHttpsUrl(url) || URLUtil.isDataUrl(url)
+                            || URLUtil.isAboutUrl(url))) {
+                        mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_MIXED;
+                    }
+                }
+            }
+        }
+
+        /**
+         * Show a dialog informing the user of the network error reported by
+         * WebCore if it is in the foreground.
+         */
+        @Override
+        public void onReceivedError(WebView view, int errorCode,
+                String description, String failingUrl) {
+            if (errorCode != WebViewClient.ERROR_HOST_LOOKUP &&
+                    errorCode != WebViewClient.ERROR_CONNECT &&
+                    errorCode != WebViewClient.ERROR_BAD_URL &&
+                    errorCode != WebViewClient.ERROR_UNSUPPORTED_SCHEME &&
+                    errorCode != WebViewClient.ERROR_FILE) {
+                queueError(errorCode, description);
+
+                // Don't log URLs when in private browsing mode
+                if (!isPrivateBrowsingEnabled()) {
+                    Log.e(LOGTAG, "onReceivedError " + errorCode + " " + failingUrl
+                        + " " + description);
+                }
+            }
+        }
+
+        /**
+         * Check with the user if it is ok to resend POST data as the page they
+         * are trying to navigate to is the result of a POST.
+         */
+        @Override
+        public void onFormResubmission(WebView view, final Message dontResend,
+                                       final Message resend) {
+            if (!mInForeground) {
+                dontResend.sendToTarget();
+                return;
+            }
+            if (mDontResend != null) {
+                Log.w(LOGTAG, "onFormResubmission should not be called again "
+                        + "while dialog is still up");
+                dontResend.sendToTarget();
+                return;
+            }
+            mDontResend = dontResend;
+            mResend = resend;
+            new AlertDialog.Builder(mContext).setTitle(
+                    R.string.browserFrameFormResubmitLabel).setMessage(
+                    R.string.browserFrameFormResubmitMessage)
+                    .setPositiveButton(R.string.ok,
+                            new DialogInterface.OnClickListener() {
+                                public void onClick(DialogInterface dialog,
+                                        int which) {
+                                    if (mResend != null) {
+                                        mResend.sendToTarget();
+                                        mResend = null;
+                                        mDontResend = null;
+                                    }
+                                }
+                            }).setNegativeButton(R.string.cancel,
+                            new DialogInterface.OnClickListener() {
+                                public void onClick(DialogInterface dialog,
+                                        int which) {
+                                    if (mDontResend != null) {
+                                        mDontResend.sendToTarget();
+                                        mResend = null;
+                                        mDontResend = null;
+                                    }
+                                }
+                            }).setOnCancelListener(new OnCancelListener() {
+                        public void onCancel(DialogInterface dialog) {
+                            if (mDontResend != null) {
+                                mDontResend.sendToTarget();
+                                mResend = null;
+                                mDontResend = null;
+                            }
+                        }
+                    }).show();
+        }
+
+        /**
+         * Insert the url into the visited history database.
+         * @param url The url to be inserted.
+         * @param isReload True if this url is being reloaded.
+         * FIXME: Not sure what to do when reloading the page.
+         */
+        @Override
+        public void doUpdateVisitedHistory(WebView view, String url,
+                boolean isReload) {
+            mWebViewController.doUpdateVisitedHistory(Tab.this, isReload);
+        }
+
+        /**
+         * Displays SSL error(s) dialog to the user.
+         */
+        @Override
+        public void onReceivedSslError(final WebView view,
+                final SslErrorHandler handler, final SslError error) {
+            if (!mInForeground) {
+                handler.cancel();
+                setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE);
+                return;
+            }
+            if (mSettings.showSecurityWarnings()) {
+                new AlertDialog.Builder(mContext)
+                    .setTitle(R.string.security_warning)
+                    .setMessage(R.string.ssl_warnings_header)
+                    .setIconAttribute(android.R.attr.alertDialogIcon)
+                    .setPositiveButton(R.string.ssl_continue,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog,
+                                    int whichButton) {
+                                handler.proceed();
+                                handleProceededAfterSslError(error);
+                            }
+                        })
+                    .setNeutralButton(R.string.view_certificate,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog,
+                                    int whichButton) {
+                                mWebViewController.showSslCertificateOnError(
+                                        view, handler, error);
+                            }
+                        })
+                    .setNegativeButton(R.string.ssl_go_back,
+                        new DialogInterface.OnClickListener() {
+                            @Override
+                            public void onClick(DialogInterface dialog,
+                                    int whichButton) {
+                                dialog.cancel();
+                            }
+                        })
+                    .setOnCancelListener(
+                        new DialogInterface.OnCancelListener() {
+                            @Override
+                            public void onCancel(DialogInterface dialog) {
+                                handler.cancel();
+                                setSecurityState(SecurityState.SECURITY_STATE_NOT_SECURE);
+                                mWebViewController.onUserCanceledSsl(Tab.this);
+                            }
+                        })
+                    .show();
+            } else {
+                handler.proceed();
+            }
+        }
+
+        /**
+         * Called when an SSL error occurred while loading a resource, but the
+         * WebView but chose to proceed anyway based on a decision retained
+         * from a previous response to onReceivedSslError(). We update our
+         * security state to reflect this.
+         */
+        @Override
+        public void onProceededAfterSslError(WebView view, SslError error) {
+            handleProceededAfterSslError(error);
+        }
+
+        /**
+         * Displays client certificate request to the user.
+         */
+        @Override
+        public void onReceivedClientCertRequest(final WebView view,
+                final ClientCertRequestHandler handler, final String host_and_port) {
+            if (!mInForeground) {
+                handler.ignore();
+                return;
+            }
+            int colon = host_and_port.lastIndexOf(':');
+            String host;
+            int port;
+            if (colon == -1) {
+                host = host_and_port;
+                port = -1;
+            } else {
+                String portString = host_and_port.substring(colon + 1);
+                try {
+                    port = Integer.parseInt(portString);
+                    host = host_and_port.substring(0, colon);
+                } catch  (NumberFormatException e) {
+                    host = host_and_port;
+                    port = -1;
+                }
+            }
+            KeyChain.choosePrivateKeyAlias(
+                    mWebViewController.getActivity(), new KeyChainAliasCallback() {
+                @Override public void alias(String alias) {
+                    if (alias == null) {
+                        handler.cancel();
+                        return;
+                    }
+                    new KeyChainLookup(mContext, handler, alias).execute();
+                }
+            }, null, null, host, port, null);
+        }
+
+        @Override
+        public void onRendererCrash(WebView view, boolean crashedWhileOomProtected) {
+            Log.e(LOGTAG, "Tab Crashed");
+            hasCrashed = true;
+            showCrashView();
+        }
+
+        /**
+         * Handles an HTTP authentication request.
+         *
+         * @param handler The authentication handler
+         * @param host The host
+         * @param realm The realm
+         */
+        @Override
+        public void onReceivedHttpAuthRequest(WebView view,
+                final HttpAuthHandler handler, final String host,
+                final String realm) {
+            mWebViewController.onReceivedHttpAuthRequest(Tab.this, view, handler, host, realm);
+        }
+
+        @Override
+        public WebResourceResponse shouldInterceptRequest(WebView view,
+                String url) {
+            //intercept if opening a new incognito tab - show the incognito welcome page
+            if (url.startsWith("browser:incognito")) {
+                Resources resourceHandle = mContext.getResources();
+                InputStream inStream = resourceHandle.openRawResource(
+                        com.android.browser.R.raw.incognito_mode_start_page);
+                return new WebResourceResponse("text/html", "utf8", inStream);
+            }
+            WebResourceResponse res;
+            if (MyNavigationUtil.MY_NAVIGATION.equals(url)) {
+                res = MyNavigationProvider.shouldInterceptRequest(mContext, url);
+            } else {
+                res = HomeProvider.shouldInterceptRequest(mContext, url);
+            }
+            return res;
+        }
+
+        @Override
+        public boolean shouldOverrideKeyEvent(WebView view, KeyEvent event) {
+            if (!mInForeground) {
+                return false;
+            }
+            return mWebViewController.shouldOverrideKeyEvent(event);
+        }
+
+        @Override
+        public void onUnhandledKeyEvent(WebView view, KeyEvent event) {
+            if (!mInForeground) {
+                return;
+            }
+            if (!mWebViewController.onUnhandledKeyEvent(event)) {
+                super.onUnhandledKeyEvent(view, event);
+            }
+        }
+
+        @Override
+        public void onReceivedLoginRequest(WebView view, String realm,
+                String account, String args) {
+            new DeviceAccountLogin(mWebViewController.getActivity(), view, Tab.this, mWebViewController)
+                    .handleLogin(realm, account, args);
+        }
+
+    };
+
+    private void syncCurrentState(WebView view, String url) {
+        // Sync state (in case of stop/timeout)
+        mCurrentState.mUrl = view.getUrl();
+        if (mCurrentState.mUrl == null) {
+            mCurrentState.mUrl = "";
+        }
+        mCurrentState.mOriginalUrl = view.getOriginalUrl();
+        mCurrentState.mTitle = view.getTitle();
+        mCurrentState.mFavicon = view.getFavicon();
+        if (!URLUtil.isHttpsUrl(mCurrentState.mUrl)) {
+            // In case we stop when loading an HTTPS page from an HTTP page
+            // but before a provisional load occurred
+            mCurrentState.mSecurityState = SecurityState.SECURITY_STATE_NOT_SECURE;
+            mCurrentState.mSslCertificateError = null;
+        }
+        mCurrentState.mIncognito = view.isPrivateBrowsingEnabled();
+    }
+
+    // Called by DeviceAccountLogin when the Tab needs to have the auto-login UI
+    // displayed.
+    void setDeviceAccountLogin(DeviceAccountLogin login) {
+        mDeviceAccountLogin = login;
+    }
+
+    // Returns non-null if the title bar should display the auto-login UI.
+    DeviceAccountLogin getDeviceAccountLogin() {
+        return mDeviceAccountLogin;
+    }
+
+    // -------------------------------------------------------------------------
+    // WebChromeClient implementation for the main WebView
+    // -------------------------------------------------------------------------
+
+    private final WebChromeClient mWebChromeClient = new WebChromeClient() {
+        // Helper method to create a new tab or sub window.
+        private void createWindow(final boolean dialog, final Message msg) {
+            WebView.WebViewTransport transport =
+                    (WebView.WebViewTransport) msg.obj;
+            if (dialog) {
+                createSubWindow();
+                mWebViewController.attachSubWindow(Tab.this);
+                transport.setWebView(mSubView);
+            } else {
+                final Tab newTab = mWebViewController.openTab(null,
+                        Tab.this, true, true);
+                transport.setWebView(newTab.getWebView());
+            }
+            msg.sendToTarget();
+        }
+
+        @Override
+        public boolean onCreateWindow(WebView view, final boolean dialog,
+                final boolean userGesture, final Message resultMsg) {
+            // only allow new window or sub window for the foreground case
+            if (!mInForeground) {
+                return false;
+            }
+            // Short-circuit if we can't create any more tabs or sub windows.
+            if (dialog && mSubView != null) {
+                new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.too_many_subwindows_dialog_title)
+                        .setIconAttribute(android.R.attr.alertDialogIcon)
+                        .setMessage(R.string.too_many_subwindows_dialog_message)
+                        .setPositiveButton(R.string.ok, null)
+                        .show();
+                return false;
+            } else if (!mWebViewController.getTabControl().canCreateNewTab()) {
+                new AlertDialog.Builder(mContext)
+                        .setTitle(R.string.too_many_windows_dialog_title)
+                        .setIconAttribute(android.R.attr.alertDialogIcon)
+                        .setMessage(R.string.too_many_windows_dialog_message)
+                        .setPositiveButton(R.string.ok, null)
+                        .show();
+                return false;
+            }
+
+            // Short-circuit if this was a user gesture.
+            if (userGesture || !mSettings.blockPopupWindows()) {
+                createWindow(dialog, resultMsg);
+                return true;
+            }
+
+            // Allow the popup and create the appropriate window.
+            final AlertDialog.OnClickListener allowListener =
+                    new AlertDialog.OnClickListener() {
+                        public void onClick(DialogInterface d,
+                                int which) {
+                            createWindow(dialog, resultMsg);
+                        }
+                    };
+
+            // Block the popup by returning a null WebView.
+            final AlertDialog.OnClickListener blockListener =
+                    new AlertDialog.OnClickListener() {
+                        public void onClick(DialogInterface d, int which) {
+                            resultMsg.sendToTarget();
+                        }
+                    };
+
+            // Build a confirmation dialog to display to the user.
+            final AlertDialog d =
+                    new AlertDialog.Builder(mContext)
+                    .setIconAttribute(android.R.attr.alertDialogIcon)
+                    .setMessage(R.string.popup_window_attempt)
+                    .setPositiveButton(R.string.allow, allowListener)
+                    .setNegativeButton(R.string.block, blockListener)
+                    .setCancelable(false)
+                    .create();
+
+            // Show the confirmation dialog.
+            d.show();
+            return true;
+        }
+
+        @Override
+        public void onRequestFocus(WebView view) {
+            if (!mInForeground) {
+                mWebViewController.switchToTab(Tab.this);
+            }
+        }
+
+        @Override
+        public void onCloseWindow(WebView window) {
+            if (mParent != null) {
+                // JavaScript can only close popup window.
+                if (mInForeground) {
+                    mWebViewController.switchToTab(mParent);
+                }
+                mWebViewController.closeTab(Tab.this);
+            }
+        }
+
+        @Override
+        public void onProgressChanged(WebView view, int newProgress) {
+            mPageLoadProgress = newProgress;
+            if (newProgress == 100) {
+                Log.i(CONSOLE_LOGTAG, "SWE Pageload Progress = 100");
+                mInPageLoad = false;
+            }
+            mWebViewController.onProgressChanged(Tab.this);
+            if (mUpdateThumbnail && newProgress == 100) {
+                mUpdateThumbnail = false;
+            }
+        }
+
+        @Override
+        public void onReceivedTitle(WebView view, final String title) {
+            mCurrentState.mTitle = title;
+            mWebViewController.onReceivedTitle(Tab.this, title);
+        }
+
+        @Override
+        public void onReceivedIcon(WebView view, Bitmap icon) {
+            mCurrentState.mFavicon = icon;
+            mWebViewController.onFavicon(Tab.this, view, icon);
+        }
+
+        @Override
+        public void onReceivedTouchIconUrl(WebView view, String url,
+                boolean precomposed) {
+            final ContentResolver cr = mContext.getContentResolver();
+            // Let precomposed icons take precedence over non-composed
+            // icons.
+            if (precomposed && mTouchIconLoader != null) {
+                mTouchIconLoader.cancel(false);
+                mTouchIconLoader = null;
+            }
+            // Have only one async task at a time.
+            if (mTouchIconLoader == null) {
+                mTouchIconLoader = new DownloadTouchIcon(Tab.this,
+                        mContext, cr, view);
+                mTouchIconLoader.execute(url);
+            }
+        }
+
+        @Override
+        public void onShowCustomView(View view,
+                CustomViewCallback callback) {
+            Activity activity = mWebViewController.getActivity();
+            if (activity != null) {
+                onShowCustomView(view, activity.getRequestedOrientation(), callback);
+            }
+        }
+
+        @Override
+        public void onShowCustomView(View view, int requestedOrientation,
+                CustomViewCallback callback) {
+            if (mInForeground) mWebViewController.showCustomView(Tab.this, view,
+                    requestedOrientation, callback);
+        }
+
+        @Override
+        public void onHideCustomView() {
+            if (mInForeground) mWebViewController.hideCustomView();
+        }
+
+        /**
+         * The origin has exceeded its database quota.
+         * @param url the URL that exceeded the quota
+         * @param databaseIdentifier the identifier of the database on which the
+         *            transaction that caused the quota overflow was run
+         * @param currentQuota the current quota for the origin.
+         * @param estimatedSize the estimated size of the database.
+         * @param totalUsedQuota is the sum of all origins' quota.
+         * @param quotaUpdater The callback to run when a decision to allow or
+         *            deny quota has been made. Don't forget to call this!
+         */
+        @Override
+        public void onExceededDatabaseQuota(String url,
+            String databaseIdentifier, long currentQuota, long estimatedSize,
+            long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+            mSettings.getWebStorageSizeManager()
+                    .onExceededDatabaseQuota(url, databaseIdentifier,
+                            currentQuota, estimatedSize, totalUsedQuota,
+                            quotaUpdater);
+        }
+
+        /**
+         * The Application Cache has exceeded its max size.
+         * @param spaceNeeded is the amount of disk space that would be needed
+         *            in order for the last appcache operation to succeed.
+         * @param totalUsedQuota is the sum of all origins' quota.
+         * @param quotaUpdater A callback to inform the WebCore thread that a
+         *            new app cache size is available. This callback must always
+         *            be executed at some point to ensure that the sleeping
+         *            WebCore thread is woken up.
+         */
+        @Override
+        public void onReachedMaxAppCacheSize(long spaceNeeded,
+                long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+            mSettings.getWebStorageSizeManager()
+                    .onReachedMaxAppCacheSize(spaceNeeded, totalUsedQuota,
+                            quotaUpdater);
+        }
+
+        /**
+         * Instructs the browser to show a prompt to ask the user to set the
+         * Geolocation permission state for the specified origin.
+         * @param origin The origin for which Geolocation permissions are
+         *     requested.
+         * @param callback The callback to call once the user has set the
+         *     Geolocation permission state.
+         */
+        @Override
+        public void onGeolocationPermissionsShowPrompt(String origin,
+                GeolocationPermissions.Callback callback) {
+            if (mInForeground) {
+                getGeolocationPermissionsPrompt().show(origin, callback);
+            }
+        }
+
+        /**
+         * Instructs the browser to hide the Geolocation permissions prompt.
+         */
+        @Override
+        public void onGeolocationPermissionsHidePrompt() {
+            if (mInForeground && mGeolocationPermissionsPrompt != null) {
+                mGeolocationPermissionsPrompt.hide();
+            }
+        }
+
+        /* Adds a JavaScript error message to the system log and if the JS
+         * console is enabled in the about:debug options, to that console
+         * also.
+         * @param consoleMessage the message object.
+         */
+        @Override
+        public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
+            if (mInForeground) {
+                // call getErrorConsole(true) so it will create one if needed
+                ErrorConsoleView errorConsole = getErrorConsole(true);
+                errorConsole.addErrorMessage(consoleMessage);
+                if (mWebViewController.shouldShowErrorConsole()
+                        && errorConsole.getShowState() !=
+                            ErrorConsoleView.SHOW_MAXIMIZED) {
+                    errorConsole.showConsole(ErrorConsoleView.SHOW_MINIMIZED);
+                }
+            }
+
+            // Don't log console messages in private browsing mode
+            if (isPrivateBrowsingEnabled()) return true;
+
+            String message = "Console: " + consoleMessage.message() + " "
+                    + consoleMessage.sourceId() +  ":"
+                    + consoleMessage.lineNumber();
+
+            switch (consoleMessage.messageLevel()) {
+                case TIP:
+                    Log.v(CONSOLE_LOGTAG, message);
+                    break;
+                case LOG:
+                    Log.i(CONSOLE_LOGTAG, message);
+                    break;
+                case WARNING:
+                    Log.w(CONSOLE_LOGTAG, message);
+                    break;
+                case ERROR:
+                    Log.e(CONSOLE_LOGTAG, message);
+                    break;
+                case DEBUG:
+                    Log.d(CONSOLE_LOGTAG, message);
+                    break;
+            }
+
+            return true;
+        }
+
+        /**
+         * Ask the browser for an icon to represent a <video> element.
+         * This icon will be used if the Web page did not specify a poster attribute.
+         * @return Bitmap The icon or null if no such icon is available.
+         */
+        @Override
+        public Bitmap getDefaultVideoPoster() {
+            if (mInForeground) {
+                return mWebViewController.getDefaultVideoPoster();
+            }
+            return null;
+        }
+
+        /**
+         * Ask the host application for a custom progress view to show while
+         * a <video> is loading.
+         * @return View The progress view.
+         */
+        @Override
+        public View getVideoLoadingProgressView() {
+            if (mInForeground) {
+                return mWebViewController.getVideoLoadingProgressView();
+            }
+            return null;
+        }
+
+        @Override
+        public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+            if (mInForeground) {
+                mWebViewController.openFileChooser(uploadMsg, acceptType, capture);
+            } else {
+                uploadMsg.onReceiveValue(null);
+            }
+        }
+
+        /**
+         * Deliver a list of already-visited URLs
+         */
+        @Override
+        public void getVisitedHistory(final ValueCallback<String[]> callback) {
+            mWebViewController.getVisitedHistory(callback);
+        }
+
+        @Override
+        public void setupAutoFill(Message message) {
+            // Prompt the user to set up their profile.
+            final Message msg = message;
+            AlertDialog.Builder builder = new AlertDialog.Builder(mContext);
+            LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                    Context.LAYOUT_INFLATER_SERVICE);
+            final View layout = inflater.inflate(R.layout.setup_autofill_dialog, null);
+
+            builder.setView(layout)
+                .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+                    @Override
+                    public void onClick(DialogInterface dialog, int id) {
+                        CheckBox disableAutoFill = (CheckBox) layout.findViewById(
+                                R.id.setup_autofill_dialog_disable_autofill);
+
+                        if (disableAutoFill.isChecked()) {
+                            // Disable autofill and show a toast with how to turn it on again.
+                            mSettings.setAutofillEnabled(false);
+                            Toast.makeText(mContext,
+                                    R.string.autofill_setup_dialog_negative_toast,
+                                    Toast.LENGTH_LONG).show();
+                        } else {
+                            // Take user to the AutoFill profile editor. When they return,
+                            // we will send the message that we pass here which will trigger
+                            // the form to get filled out with their new profile.
+                            mWebViewController.setupAutoFill(msg);
+                        }
+                    }
+                })
+                .setNegativeButton(R.string.cancel, null)
+                .show();
+        }
+    };
+
+    // -------------------------------------------------------------------------
+    // WebViewClient implementation for the sub window
+    // -------------------------------------------------------------------------
+
+    // Subclass of WebViewClient used in subwindows to notify the main
+    // WebViewClient of certain WebView activities.
+    private static class SubWindowClient extends WebViewClient {
+        // The main WebViewClient.
+        private final WebViewClient mClient;
+        private final WebViewController mController;
+
+        SubWindowClient(WebViewClient client, WebViewController controller) {
+            mClient = client;
+            mController = controller;
+        }
+        @Override
+        public void onPageStarted(WebView view, String url, Bitmap favicon) {
+            // Unlike the others, do not call mClient's version, which would
+            // change the progress bar.  However, we do want to remove the
+            // find or select dialog.
+            mController.endActionMode();
+        }
+        @Override
+        public void doUpdateVisitedHistory(WebView view, String url,
+                boolean isReload) {
+            mClient.doUpdateVisitedHistory(view, url, isReload);
+        }
+        @Override
+        public boolean shouldOverrideUrlLoading(WebView view, String url) {
+            return mClient.shouldOverrideUrlLoading(view, url);
+        }
+        @Override
+        public void onReceivedSslError(WebView view, SslErrorHandler handler,
+                SslError error) {
+            mClient.onReceivedSslError(view, handler, error);
+        }
+        @Override
+        public void onReceivedClientCertRequest(WebView view,
+                ClientCertRequestHandler handler, String host_and_port) {
+            mClient.onReceivedClientCertRequest(view, handler, host_and_port);
+        }
+        @Override
+        public void onReceivedHttpAuthRequest(WebView view,
+                HttpAuthHandler handler, String host, String realm) {
+            mClient.onReceivedHttpAuthRequest(view, handler, host, realm);
+        }
+        @Override
+        public void onFormResubmission(WebView view, Message dontResend,
+                Message resend) {
+            mClient.onFormResubmission(view, dontResend, resend);
+        }
+        @Override
+        public void onReceivedError(WebView view, int errorCode,
+                String description, String failingUrl) {
+            mClient.onReceivedError(view, errorCode, description, failingUrl);
+        }
+        @Override
+        public boolean shouldOverrideKeyEvent(WebView view,
+                android.view.KeyEvent event) {
+            return mClient.shouldOverrideKeyEvent(view, event);
+        }
+        @Override
+        public void onUnhandledKeyEvent(WebView view,
+                android.view.KeyEvent event) {
+            mClient.onUnhandledKeyEvent(view, event);
+        }
+    }
+
+    // -------------------------------------------------------------------------
+    // WebChromeClient implementation for the sub window
+    // -------------------------------------------------------------------------
+
+    private class SubWindowChromeClient extends WebChromeClient {
+        // The main WebChromeClient.
+        private final WebChromeClient mClient;
+
+        SubWindowChromeClient(WebChromeClient client) {
+            mClient = client;
+        }
+        @Override
+        public void onProgressChanged(WebView view, int newProgress) {
+            mClient.onProgressChanged(view, newProgress);
+        }
+        @Override
+        public boolean onCreateWindow(WebView view, boolean dialog,
+                boolean userGesture, android.os.Message resultMsg) {
+            return mClient.onCreateWindow(view, dialog, userGesture, resultMsg);
+        }
+        @Override
+        public void onCloseWindow(WebView window) {
+            if (window != mSubView) {
+                Log.e(LOGTAG, "Can't close the window");
+            }
+            mWebViewController.dismissSubWindow(Tab.this);
+        }
+    }
+
+    // -------------------------------------------------------------------------
+
+    // Construct a new tab
+    Tab(WebViewController wvcontroller, WebView w) {
+        this(wvcontroller, w, null);
+    }
+
+    Tab(WebViewController wvcontroller, Bundle state) {
+        this(wvcontroller, null, state);
+    }
+
+    Tab(WebViewController wvcontroller, WebView w, Bundle state) {
+        mWebViewController = wvcontroller;
+        mContext = mWebViewController.getContext();
+        mSettings = BrowserSettings.getInstance();
+        mDataController = DataController.getInstance(mContext);
+        mCurrentState = new PageState(mContext, w != null
+                ? w.isPrivateBrowsingEnabled() : false);
+        mInPageLoad = false;
+        mInForeground = false;
+
+        mDownloadListener = new BrowserDownloadListener() {
+            public void onDownloadStart(String url, String userAgent,
+                    String contentDisposition, String mimetype, String referer,
+                    long contentLength) {
+                mWebViewController.onDownloadStart(Tab.this, url, userAgent, contentDisposition,
+                        mimetype, referer, contentLength);
+            }
+        };
+        mWebBackForwardListClient = new WebBackForwardListClient() {
+            @Override
+            public void onNewHistoryItem(WebHistoryItem item) {
+                if (mClearHistoryUrlPattern != null) {
+                    boolean match =
+                        mClearHistoryUrlPattern.matcher(item.getOriginalUrl()).matches();
+                    if (LOGD_ENABLED) {
+                        Log.d(LOGTAG, "onNewHistoryItem: match=" + match + "\n\t"
+                                + item.getUrl() + "\n\t"
+                                + mClearHistoryUrlPattern);
+                    }
+                    if (match) {
+                        if (mMainView != null) {
+                            mMainView.clearHistory();
+                        }
+                    }
+                    mClearHistoryUrlPattern = null;
+                }
+            }
+        };
+
+        mCaptureWidth = mContext.getResources().getDimensionPixelSize(
+                R.dimen.tab_thumbnail_width);
+        mCaptureHeight = mContext.getResources().getDimensionPixelSize(
+                R.dimen.tab_thumbnail_height);
+        updateShouldCaptureThumbnails();
+        restoreState(state);
+        if (getId() == -1) {
+            mId = TabControl.getNextId();
+        }
+        setWebView(w);
+        mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message m) {
+                switch (m.what) {
+                case MSG_CAPTURE:
+                    capture();
+                    break;
+                }
+            }
+        };
+    }
+
+    public boolean shouldUpdateThumbnail() {
+        return mUpdateThumbnail;
+    }
+
+    /**
+     * This is used to get a new ID when the tab has been preloaded, before it is displayed and
+     * added to TabControl. Preloaded tabs can be created before restoreInstanceState, leading
+     * to overlapping IDs between the preloaded and restored tabs.
+     */
+    public void refreshIdAfterPreload() {
+        mId = TabControl.getNextId();
+    }
+
+    public void updateShouldCaptureThumbnails() {
+        if (mWebViewController.shouldCaptureThumbnails()) {
+            synchronized (Tab.this) {
+                if (mCapture == null) {
+                    mCapture = Bitmap.createBitmap(mCaptureWidth, mCaptureHeight,
+                            Bitmap.Config.RGB_565);
+                    mCapture.eraseColor(Color.WHITE);
+                    if (mInForeground) {
+                        postCapture();
+                    }
+                }
+            }
+        } else {
+            synchronized (Tab.this) {
+                mCapture = null;
+                deleteThumbnail();
+            }
+        }
+    }
+
+    public void setController(WebViewController ctl) {
+        mWebViewController = ctl;
+        updateShouldCaptureThumbnails();
+    }
+
+    public long getId() {
+        return mId;
+    }
+
+    void setWebView(WebView w) {
+        setWebView(w, true);
+    }
+
+    public boolean isNativeActive(){
+        if (mMainView == null)
+            return false;
+        return true;
+    }
+
+    public void setTimeStamp(){
+        Date d = new Date();
+        timestamp = (new Timestamp(d.getTime()));
+    }
+
+    public Timestamp getTimestamp() {
+        return timestamp;
+    }
+    /**
+     * Sets the WebView for this tab, correctly removing the old WebView from
+     * the container view.
+     */
+    void setWebView(WebView w, boolean restore) {
+        if (mMainView == w) {
+            return;
+        }
+
+        // If the WebView is changing, the page will be reloaded, so any ongoing
+        // Geolocation permission requests are void.
+        if (mGeolocationPermissionsPrompt != null) {
+            mGeolocationPermissionsPrompt.hide();
+        }
+
+        mWebViewController.onSetWebView(this, w);
+
+        if (mMainView != null) {
+            mMainView.setPictureListener(null);
+            if (w != null) {
+                syncCurrentState(w, null);
+            } else {
+                mCurrentState = new PageState(mContext, mMainView.isPrivateBrowsingEnabled());
+            }
+        }
+        // set the new one
+        mMainView = w;
+        // attach the WebViewClient, WebChromeClient and DownloadListener
+        if (mMainView != null) {
+            mMainView.setWebViewClient(mWebViewClient);
+            mMainView.setWebChromeClient(mWebChromeClient);
+            // Attach DownloadManager so that downloads can start in an active
+            // or a non-active window. This can happen when going to a site that
+            // does a redirect after a period of time. The user could have
+            // switched to another tab while waiting for the download to start.
+            mMainView.setDownloadListener(mDownloadListener);
+            getWebView().setWebBackForwardListClient(mWebBackForwardListClient);
+            TabControl tc = mWebViewController.getTabControl();
+            if (tc != null /*&& tc.getOnThumbnailUpdatedListener() != null*/) {
+                mMainView.setPictureListener(this);
+            }
+            if (restore && (mSavedState != null)) {
+                restoreUserAgent();
+                WebBackForwardList restoredState
+                        = mMainView.restoreState(mSavedState);
+                if (restoredState == null || restoredState.getSize() == 0) {
+                    Log.w(LOGTAG, "Failed to restore WebView state!");
+                    loadUrl(mCurrentState.mOriginalUrl, null);
+                }
+                mSavedState = null;
+            }
+        }
+    }
+
+    /**
+     * Destroy the tab's main WebView and subWindow if any
+     */
+    void destroy() {
+        if (mMainView != null) {
+            dismissSubWindow();
+            // save the WebView to call destroy() after detach it from the tab
+            WebView webView = mMainView;
+            setWebView(null);
+            webView.destroy();
+        }
+    }
+
+    /**
+     * Remove the tab from the parent
+     */
+    void removeFromTree() {
+        // detach the children
+        if (mChildren != null) {
+            for(Tab t : mChildren) {
+                t.setParent(null);
+            }
+        }
+        // remove itself from the parent list
+        if (mParent != null) {
+            mParent.mChildren.remove(this);
+        }
+        deleteThumbnail();
+    }
+
+    /**
+     * Create a new subwindow unless a subwindow already exists.
+     * @return True if a new subwindow was created. False if one already exists.
+     */
+    boolean createSubWindow() {
+        if (mSubView == null) {
+            mWebViewController.createSubWindow(this);
+            mSubView.setWebViewClient(new SubWindowClient(mWebViewClient,
+                    mWebViewController));
+            mSubView.setWebChromeClient(new SubWindowChromeClient(
+                    mWebChromeClient));
+            // Set a different DownloadListener for the mSubView, since it will
+            // just need to dismiss the mSubView, rather than close the Tab
+            mSubView.setDownloadListener(new BrowserDownloadListener() {
+                public void onDownloadStart(String url, String userAgent,
+                        String contentDisposition, String mimetype, String referer,
+                        long contentLength) {
+                    mWebViewController.onDownloadStart(Tab.this, url, userAgent,
+                            contentDisposition, mimetype, referer, contentLength);
+                    if (mSubView.copyBackForwardList().getSize() == 0) {
+                        // This subwindow was opened for the sole purpose of
+                        // downloading a file. Remove it.
+                        mWebViewController.dismissSubWindow(Tab.this);
+                    }
+                }
+            });
+            mSubView.setOnCreateContextMenuListener(mWebViewController.getActivity());
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Dismiss the subWindow for the tab.
+     */
+    void dismissSubWindow() {
+        if (mSubView != null) {
+            mWebViewController.endActionMode();
+            mSubView.destroy();
+            mSubView = null;
+            mSubViewContainer = null;
+        }
+    }
+
+
+    /**
+     * Set the parent tab of this tab.
+     */
+    void setParent(Tab parent) {
+        if (parent == this) {
+            throw new IllegalStateException("Cannot set parent to self!");
+        }
+        mParent = parent;
+        // This tab may have been freed due to low memory. If that is the case,
+        // the parent tab id is already saved. If we are changing that id
+        // (most likely due to removing the parent tab) we must update the
+        // parent tab id in the saved Bundle.
+        if (mSavedState != null) {
+            if (parent == null) {
+                mSavedState.remove(PARENTTAB);
+            } else {
+                mSavedState.putLong(PARENTTAB, parent.getId());
+            }
+        }
+
+        // Sync the WebView useragent with the parent
+        if (parent != null && mSettings.hasDesktopUseragent(parent.getWebView())
+                != mSettings.hasDesktopUseragent(getWebView())) {
+            mSettings.toggleDesktopUseragent(getWebView());
+        }
+
+        if (parent != null && parent.getId() == getId()) {
+            throw new IllegalStateException("Parent has same ID as child!");
+        }
+    }
+
+    /**
+     * If this Tab was created through another Tab, then this method returns
+     * that Tab.
+     * @return the Tab parent or null
+     */
+    public Tab getParent() {
+        return mParent;
+    }
+
+    /**
+     * When a Tab is created through the content of another Tab, then we
+     * associate the Tabs.
+     * @param child the Tab that was created from this Tab
+     */
+    void addChildTab(Tab child) {
+        if (mChildren == null) {
+            mChildren = new Vector<Tab>();
+        }
+        mChildren.add(child);
+        child.setParent(this);
+    }
+
+    Vector<Tab> getChildren() {
+        return mChildren;
+    }
+
+    void resume() {
+        if (mMainView != null) {
+            setupHwAcceleration(mMainView);
+            mMainView.onResume();
+            if (mSubView != null) {
+                mSubView.onResume();
+            }
+        }
+    }
+
+    private void setupHwAcceleration(View web) {
+        if (web == null) return;
+        BrowserSettings settings = BrowserSettings.getInstance();
+        if (settings.isHardwareAccelerated()) {
+            web.setLayerType(View.LAYER_TYPE_NONE, null);
+        } else {
+            web.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+        }
+    }
+
+    void pause() {
+        if (mMainView != null) {
+            mMainView.onPause();
+            if (mSubView != null) {
+                mSubView.onPause();
+            }
+        }
+    }
+
+    void putInForeground() {
+        if (mInForeground) {
+            return;
+        }
+        mInForeground = true;
+        resume();
+        Activity activity = mWebViewController.getActivity();
+        mMainView.setOnCreateContextMenuListener(activity);
+        if (mSubView != null) {
+            mSubView.setOnCreateContextMenuListener(activity);
+        }
+        // Show the pending error dialog if the queue is not empty
+        if (mQueuedErrors != null && mQueuedErrors.size() >  0) {
+            showError(mQueuedErrors.getFirst());
+        }
+        mWebViewController.bookmarkedStatusHasChanged(this);
+    }
+
+    void putInBackground() {
+        if (!mInForeground) {
+            return;
+        }
+        capture();
+        mInForeground = false;
+        pause();
+        mMainView.setOnCreateContextMenuListener(null);
+        if (mSubView != null) {
+            mSubView.setOnCreateContextMenuListener(null);
+        }
+    }
+
+    boolean inForeground() {
+        return mInForeground;
+    }
+
+    /**
+     * Return the top window of this tab; either the subwindow if it is not
+     * null or the main window.
+     * @return The top window of this tab.
+     */
+    WebView getTopWindow() {
+        if (mSubView != null) {
+            return mSubView;
+        }
+        return mMainView;
+    }
+
+    /**
+     * Return the main window of this tab. Note: if a tab is freed in the
+     * background, this can return null. It is only guaranteed to be
+     * non-null for the current tab.
+     * @return The main WebView of this tab.
+     */
+    WebView getWebView() {
+        return mMainView;
+    }
+
+    void setViewContainer(View container) {
+        mContainer = container;
+    }
+
+    View getViewContainer() {
+        return mContainer;
+    }
+
+    /**
+     * Return whether private browsing is enabled for the main window of
+     * this tab.
+     * @return True if private browsing is enabled.
+     */
+    boolean isPrivateBrowsingEnabled() {
+        return mCurrentState.mIncognito;
+    }
+
+    /**
+     * Return the subwindow of this tab or null if there is no subwindow.
+     * @return The subwindow of this tab or null.
+     */
+    WebView getSubWebView() {
+        return mSubView;
+    }
+
+    void setSubWebView(WebView subView) {
+        mSubView = subView;
+    }
+
+    View getSubViewContainer() {
+        return mSubViewContainer;
+    }
+
+    void setSubViewContainer(View subViewContainer) {
+        mSubViewContainer = subViewContainer;
+    }
+
+    /**
+     * @return The geolocation permissions prompt for this tab.
+     */
+    GeolocationPermissionsPrompt getGeolocationPermissionsPrompt() {
+        if (mGeolocationPermissionsPrompt == null) {
+            ViewStub stub = (ViewStub) mContainer
+                    .findViewById(R.id.geolocation_permissions_prompt);
+            mGeolocationPermissionsPrompt = (GeolocationPermissionsPrompt) stub
+                    .inflate();
+        }
+        return mGeolocationPermissionsPrompt;
+    }
+
+    /**
+     * @return The application id string
+     */
+    String getAppId() {
+        return mAppId;
+    }
+
+    /**
+     * Set the application id string
+     * @param id
+     */
+    void setAppId(String id) {
+        mAppId = id;
+    }
+
+    boolean closeOnBack() {
+        return mCloseOnBack;
+    }
+
+    void setCloseOnBack(boolean close) {
+        mCloseOnBack = close;
+    }
+
+    String getUrl() {
+        return UrlUtils.filteredUrl(mCurrentState.mUrl);
+    }
+
+    String getOriginalUrl() {
+        if (mCurrentState.mOriginalUrl == null) {
+            return getUrl();
+        }
+        return UrlUtils.filteredUrl(mCurrentState.mOriginalUrl);
+    }
+
+    /**
+     * Get the title of this tab.
+     */
+    String getTitle() {
+        if (mCurrentState.mTitle == null && mInPageLoad) {
+            return mContext.getString(R.string.title_bar_loading);
+        }
+        return mCurrentState.mTitle;
+    }
+
+    /**
+     * Get the favicon of this tab.
+     */
+    Bitmap getFavicon() {
+        if (mCurrentState.mFavicon != null) {
+            return mCurrentState.mFavicon;
+        }
+        return getDefaultFavicon(mContext);
+    }
+
+    public boolean isBookmarkedSite() {
+        return mCurrentState.mIsBookmarkedSite;
+    }
+
+    /**
+     * Return the tab's error console. Creates the console if createIfNEcessary
+     * is true and we haven't already created the console.
+     * @param createIfNecessary Flag to indicate if the console should be
+     *            created if it has not been already.
+     * @return The tab's error console, or null if one has not been created and
+     *         createIfNecessary is false.
+     */
+    ErrorConsoleView getErrorConsole(boolean createIfNecessary) {
+        if (createIfNecessary && mErrorConsole == null) {
+            mErrorConsole = new ErrorConsoleView(mContext);
+            mErrorConsole.setWebView(mMainView);
+        }
+        return mErrorConsole;
+    }
+
+    /**
+     * Sets the security state, clears the SSL certificate error and informs
+     * the controller.
+     */
+    private void setSecurityState(SecurityState securityState) {
+        mCurrentState.mSecurityState = securityState;
+        mCurrentState.mSslCertificateError = null;
+        mWebViewController.onUpdatedSecurityState(this);
+    }
+
+    /**
+     * @return The tab's security state.
+     */
+    SecurityState getSecurityState() {
+        return mCurrentState.mSecurityState;
+    }
+
+    /**
+     * Gets the SSL certificate error, if any, for the page's main resource.
+     * This is only non-null when the security state is
+     * SECURITY_STATE_BAD_CERTIFICATE.
+     */
+    SslError getSslCertificateError() {
+        return mCurrentState.mSslCertificateError;
+    }
+
+    int getLoadProgress() {
+        if (mInPageLoad) {
+            return mPageLoadProgress;
+        }
+        return 100;
+    }
+
+    /**
+     * @return TRUE if onPageStarted is called while onPageFinished is not
+     *         called yet.
+     */
+    boolean inPageLoad() {
+        return mInPageLoad;
+    }
+
+    /**
+     * @return The Bundle with the tab's state if it can be saved, otherwise null
+     */
+    public Bundle saveState() {
+        // If the WebView is null it means we ran low on memory and we already
+        // stored the saved state in mSavedState.
+        if (mMainView == null) {
+            return mSavedState;
+        }
+
+        if (TextUtils.isEmpty(mCurrentState.mUrl)) {
+            return null;
+        }
+
+        mSavedState = new Bundle();
+        WebBackForwardList savedList = mMainView.saveState(mSavedState);
+        if (savedList == null || savedList.getSize() == 0) {
+            Log.w(LOGTAG, "Failed to save back/forward list for "
+                    + mCurrentState.mUrl);
+        }
+
+        mSavedState.putLong(ID, mId);
+        mSavedState.putString(CURRURL, mCurrentState.mUrl);
+        mSavedState.putString(CURRTITLE, mCurrentState.mTitle);
+        mSavedState.putBoolean(INCOGNITO, mMainView.isPrivateBrowsingEnabled());
+        if (mAppId != null) {
+            mSavedState.putString(APPID, mAppId);
+        }
+        mSavedState.putBoolean(CLOSEFLAG, mCloseOnBack);
+        // Remember the parent tab so the relationship can be restored.
+        if (mParent != null) {
+            mSavedState.putLong(PARENTTAB, mParent.mId);
+        }
+        mSavedState.putBoolean(USERAGENT,
+                mSettings.hasDesktopUseragent(getWebView()));
+        return mSavedState;
+    }
+
+    /*
+     * Restore the state of the tab.
+     */
+    private void restoreState(Bundle b) {
+        mSavedState = b;
+        if (mSavedState == null) {
+            return;
+        }
+        // Restore the internal state even if the WebView fails to restore.
+        // This will maintain the app id, original url and close-on-exit values.
+        mId = b.getLong(ID);
+        mAppId = b.getString(APPID);
+        mCloseOnBack = b.getBoolean(CLOSEFLAG);
+        restoreUserAgent();
+        String url = b.getString(CURRURL);
+        String title = b.getString(CURRTITLE);
+        boolean incognito = b.getBoolean(INCOGNITO);
+        mCurrentState = new PageState(mContext, incognito, url, null);
+        mCurrentState.mTitle = title;
+        synchronized (Tab.this) {
+            if (mCapture != null) {
+                DataController.getInstance(mContext).loadThumbnail(this);
+            }
+        }
+    }
+
+    private void restoreUserAgent() {
+        if (mMainView == null || mSavedState == null) {
+            return;
+        }
+        if (mSavedState.getBoolean(USERAGENT)
+                != mSettings.hasDesktopUseragent(mMainView)) {
+            mSettings.toggleDesktopUseragent(mMainView);
+        }
+    }
+
+    public void updateBookmarkedStatus() {
+        mDataController.queryBookmarkStatus(getUrl(), mIsBookmarkCallback);
+    }
+
+    private DataController.OnQueryUrlIsBookmark mIsBookmarkCallback
+            = new DataController.OnQueryUrlIsBookmark() {
+        @Override
+        public void onQueryUrlIsBookmark(String url, boolean isBookmark) {
+            if (mCurrentState.mUrl.equals(url)) {
+                mCurrentState.mIsBookmarkedSite = isBookmark;
+                mWebViewController.bookmarkedStatusHasChanged(Tab.this);
+            }
+        }
+    };
+
+    public Bitmap getScreenshot() {
+        synchronized (Tab.this) {
+            return mCapture;
+        }
+    }
+
+    public boolean isSnapshot() {
+        return false;
+    }
+
+    private static class SaveCallback implements ValueCallback<String> {
+        boolean onReceiveValueCalled = false;
+        private String mPath;
+
+        @Override
+        public void onReceiveValue(String path) {
+            this.onReceiveValueCalled = true;
+            this.mPath = path;
+            synchronized (this) {
+                notifyAll();
+            }
+        }
+
+        public String getPath() {
+          return mPath;
+        }
+    }
+
+    /**
+     * Must be called on the UI thread
+     */
+    public ContentValues createSnapshotValues() {
+        WebView web = getWebView();
+        if (web == null) return null;
+        ContentValues values = new ContentValues();
+        values.put(Snapshots.TITLE, mCurrentState.mTitle);
+        values.put(Snapshots.URL, mCurrentState.mUrl);
+        values.put(Snapshots.BACKGROUND, web.getPageBackgroundColor());
+        values.put(Snapshots.DATE_CREATED, System.currentTimeMillis());
+        values.put(Snapshots.FAVICON, compressBitmap(getFavicon()));
+        Bitmap screenshot = web.getViewportBitmap();
+        values.put(Snapshots.THUMBNAIL, compressBitmap(screenshot));
+        return values;
+    }
+
+    /**
+     * Probably want to call this on a background thread
+     */
+    public boolean saveViewState(ContentValues values) {
+        WebView web = getWebView();
+        if (web == null) return false;
+        String filename = UUID.randomUUID().toString();
+        SaveCallback callback = new SaveCallback();
+        try {
+            synchronized (callback) {
+               web.saveViewState(filename, callback);
+               callback.wait();
+            }
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Failed to save view state", e);
+            String path = callback.getPath();
+            if (path != null) {
+                File file = mContext.getFileStreamPath(path);
+                if (file.exists() && !file.delete()) {
+                    file.deleteOnExit();
+                }
+            }
+            return false;
+        }
+        String path = callback.getPath();
+        File savedFile = new File(path);
+        if (!savedFile.exists()) {
+           return false;
+        }
+        values.put(Snapshots.VIEWSTATE_PATH, path.substring(path.lastIndexOf('/') + 1));
+        values.put(Snapshots.VIEWSTATE_SIZE, savedFile.length());
+        return true;
+    }
+
+    public byte[] compressBitmap(Bitmap bitmap) {
+        if (bitmap == null) {
+            return null;
+        }
+        ByteArrayOutputStream stream = new ByteArrayOutputStream();
+        bitmap.compress(CompressFormat.PNG, 100, stream);
+        return stream.toByteArray();
+    }
+
+    public void loadUrl(String url, Map<String, String> headers) {
+        if (mMainView != null) {
+            mPageLoadProgress = INITIAL_PROGRESS;
+            mInPageLoad = true;
+            mCurrentState = new PageState(mContext, false, url, null);
+            mWebViewController.onPageStarted(this, mMainView, null);
+            mMainView.loadUrl(url, headers);
+        }
+    }
+
+    public void disableUrlOverridingForLoad() {
+        mDisableOverrideUrlLoading = true;
+    }
+
+    protected void capture() {
+        if (mMainView == null || mCapture == null) return;
+        if (mMainView.getContentWidth() <= 0 || mMainView.getContentHeight() <= 0) {
+            return;
+        }
+        Canvas c = new Canvas(mCapture);
+        int state = c.save();
+        Bitmap screenShot = mMainView.getViewportBitmap();
+        if (screenShot != null) {
+           mCapture.eraseColor(Color.WHITE);
+           float scale = (float) mCaptureWidth / screenShot.getWidth();
+           c.scale(scale, scale);
+           c.drawBitmap(screenShot, 0, 0, null);
+         } else {
+             final int left = mMainView.getViewScrollX();
+             final int top = mMainView.getViewScrollY() +  mMainView.getVisibleTitleHeight();
+             c.translate(-left, -top);
+             float scale = mCaptureWidth / (float) mMainView.getWidth();
+             c.scale(scale, scale, left, top);
+             if (mMainView instanceof BrowserWebView) {
+                ((BrowserWebView)mMainView).drawContent(c);
+             } else {
+                mMainView.draw(c);
+             }
+        }
+        c.restoreToCount(state);
+        // manually anti-alias the edges for the tilt
+        c.drawRect(0, 0, 1, mCapture.getHeight(), sAlphaPaint);
+        c.drawRect(mCapture.getWidth() - 1, 0, mCapture.getWidth(),
+                mCapture.getHeight(), sAlphaPaint);
+        c.drawRect(0, 0, mCapture.getWidth(), 1, sAlphaPaint);
+        c.drawRect(0, mCapture.getHeight() - 1, mCapture.getWidth(),
+                mCapture.getHeight(), sAlphaPaint);
+        c.setBitmap(null);
+        mHandler.removeMessages(MSG_CAPTURE);
+        persistThumbnail();
+        TabControl tc = mWebViewController.getTabControl();
+        if (tc != null) {
+            OnThumbnailUpdatedListener updateListener
+                    = tc.getOnThumbnailUpdatedListener();
+            if (updateListener != null) {
+                updateListener.onThumbnailUpdated(this);
+            }
+        }
+    }
+
+    @Override
+    public void onNewPicture(WebView view, Picture picture) {
+        postCapture();
+    }
+
+    private void postCapture() {
+        if (!mHandler.hasMessages(MSG_CAPTURE)) {
+            mHandler.sendEmptyMessageDelayed(MSG_CAPTURE, CAPTURE_DELAY);
+        }
+    }
+
+    public boolean canGoBack() {
+        return mMainView != null ? mMainView.canGoBack() : false;
+    }
+
+    public boolean canGoForward() {
+        return mMainView != null ? mMainView.canGoForward() : false;
+    }
+
+    public void goBack() {
+        if (mMainView != null) {
+            mMainView.goBack();
+        }
+    }
+
+    public void goForward() {
+        if (mMainView != null) {
+            mMainView.goForward();
+        }
+    }
+
+    /**
+     * Causes the tab back/forward stack to be cleared once, if the given URL is the next URL
+     * to be added to the stack.
+     *
+     * This is used to ensure that preloaded URLs that are not subsequently seen by the user do
+     * not appear in the back stack.
+     */
+    public void clearBackStackWhenItemAdded(Pattern urlPattern) {
+        mClearHistoryUrlPattern = urlPattern;
+    }
+
+    protected void persistThumbnail() {
+        DataController.getInstance(mContext).saveThumbnail(this);
+    }
+
+    protected void deleteThumbnail() {
+        DataController.getInstance(mContext).deleteThumbnail(this);
+    }
+
+    void updateCaptureFromBlob(byte[] blob) {
+        synchronized (Tab.this) {
+            if (mCapture == null) {
+                return;
+            }
+            ByteBuffer buffer = ByteBuffer.wrap(blob);
+            try {
+                mCapture.copyPixelsFromBuffer(buffer);
+            } catch (RuntimeException rex) {
+                Log.e(LOGTAG, "Load capture has mismatched sizes; buffer: "
+                        + buffer.capacity() + " blob: " + blob.length
+                        + "capture: " + mCapture.getByteCount());
+                throw rex;
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder(100);
+        builder.append(mId);
+        builder.append(") has parent: ");
+        if (getParent() != null) {
+            builder.append("true[");
+            builder.append(getParent().getId());
+            builder.append("]");
+        } else {
+            builder.append("false");
+        }
+        builder.append(", incog: ");
+        builder.append(isPrivateBrowsingEnabled());
+        if (!isPrivateBrowsingEnabled()) {
+            builder.append(", title: ");
+            builder.append(getTitle());
+            builder.append(", url: ");
+            builder.append(getUrl());
+        }
+        return builder.toString();
+    }
+
+    private void handleProceededAfterSslError(SslError error) {
+        if (error.getUrl().equals(mCurrentState.mUrl)) {
+            // The security state should currently be SECURITY_STATE_SECURE.
+            setSecurityState(SecurityState.SECURITY_STATE_BAD_CERTIFICATE);
+            mCurrentState.mSslCertificateError = error;
+        } else if (getSecurityState() == SecurityState.SECURITY_STATE_SECURE) {
+            // The page's main resource is secure and this error is for a
+            // sub-resource.
+            setSecurityState(SecurityState.SECURITY_STATE_MIXED);
+        }
+    }
+}
diff --git a/src/com/android/browser/TabBar.java b/src/com/android/browser/TabBar.java
new file mode 100644
index 0000000..4078ba4
--- /dev/null
+++ b/src/com/android/browser/TabBar.java
@@ -0,0 +1,536 @@
+/*
+ * Copyright (C) 2010 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.app.Activity;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Shader;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.android.browser.R;
+
+/**
+ * tabbed title bar for xlarge screen browser
+ */
+public class TabBar extends LinearLayout implements OnClickListener {
+
+    private static final int PROGRESS_MAX = 100;
+
+    private Activity mActivity;
+    private UiController mUiController;
+    private TabControl mTabControl;
+    private XLargeUi mUi;
+
+    private int mTabWidth;
+
+    private TabScrollView mTabs;
+
+    private ImageButton mNewTab;
+    private int mButtonWidth;
+
+    private Map<Tab, TabView> mTabMap;
+
+    private int mCurrentTextureWidth = 0;
+    private int mCurrentTextureHeight = 0;
+
+    private Drawable mActiveDrawable;
+    private Drawable mInactiveDrawable;
+
+    private final Paint mActiveShaderPaint = new Paint();
+    private final Paint mInactiveShaderPaint = new Paint();
+    private final Paint mFocusPaint = new Paint();
+    private final Matrix mActiveMatrix = new Matrix();
+    private final Matrix mInactiveMatrix = new Matrix();
+
+    private BitmapShader mActiveShader;
+    private BitmapShader mInactiveShader;
+
+    private int mTabOverlap;
+    private int mAddTabOverlap;
+    private int mTabSliceWidth;
+    private boolean mUseQuickControls;
+
+    public TabBar(Activity activity, UiController controller, XLargeUi ui) {
+        super(activity);
+        mActivity = activity;
+        mUiController = controller;
+        mTabControl = mUiController.getTabControl();
+        mUi = ui;
+        Resources res = activity.getResources();
+        mTabWidth = (int) res.getDimension(R.dimen.tab_width);
+        mActiveDrawable = res.getDrawable(R.drawable.bg_urlbar);
+        mInactiveDrawable = res.getDrawable(R.drawable.browsertab_inactive);
+
+        mTabMap = new HashMap<Tab, TabView>();
+        LayoutInflater factory = LayoutInflater.from(activity);
+        factory.inflate(R.layout.tab_bar, this);
+        setPadding(0, (int) res.getDimension(R.dimen.tab_padding_top), 0, 0);
+        mTabs = (TabScrollView) findViewById(R.id.tabs);
+        mNewTab = (ImageButton) findViewById(R.id.newtab);
+        mNewTab.setOnClickListener(this);
+
+        updateTabs(mUiController.getTabs());
+        mButtonWidth = -1;
+        // tab dimensions
+        mTabOverlap = (int) res.getDimension(R.dimen.tab_overlap);
+        mAddTabOverlap = (int) res.getDimension(R.dimen.tab_addoverlap);
+        mTabSliceWidth = (int) res.getDimension(R.dimen.tab_slice);
+
+        mActiveShaderPaint.setStyle(Paint.Style.FILL);
+        mActiveShaderPaint.setAntiAlias(true);
+
+        mInactiveShaderPaint.setStyle(Paint.Style.FILL);
+        mInactiveShaderPaint.setAntiAlias(true);
+
+        mFocusPaint.setStyle(Paint.Style.STROKE);
+        mFocusPaint.setStrokeWidth(res.getDimension(R.dimen.tab_focus_stroke));
+        mFocusPaint.setAntiAlias(true);
+        mFocusPaint.setColor(res.getColor(R.color.tabFocusHighlight));
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        Resources res = mActivity.getResources();
+        mTabWidth = (int) res.getDimension(R.dimen.tab_width);
+        // force update of tab bar
+        mTabs.updateLayout();
+    }
+
+    void setUseQuickControls(boolean useQuickControls) {
+        mUseQuickControls = useQuickControls;
+        mNewTab.setVisibility(mUseQuickControls ? View.GONE
+                : View.VISIBLE);
+    }
+
+    int getTabCount() {
+        return mTabMap.size();
+    }
+
+    void updateTabs(List<Tab> tabs) {
+        mTabs.clearTabs();
+        mTabMap.clear();
+        for (Tab tab : tabs) {
+            TabView tv = buildTabView(tab);
+            mTabs.addTab(tv);
+        }
+        mTabs.setSelectedTab(mTabControl.getCurrentPosition());
+    }
+
+    @Override
+    protected void onMeasure(int hspec, int vspec) {
+        super.onMeasure(hspec, vspec);
+        int w = getMeasuredWidth();
+        // adjust for new tab overlap
+        if (!mUseQuickControls) {
+            w -= mAddTabOverlap;
+        }
+        setMeasuredDimension(w, getMeasuredHeight());
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        // use paddingLeft and paddingTop
+        int pl = getPaddingLeft();
+        int pt = getPaddingTop();
+        int sw = mTabs.getMeasuredWidth();
+        int w = right - left - pl;
+        if (mUseQuickControls) {
+            mButtonWidth = 0;
+        } else {
+            mButtonWidth = mNewTab.getMeasuredWidth() - mAddTabOverlap;
+            if (w-sw < mButtonWidth) {
+                sw = w - mButtonWidth;
+            }
+        }
+        mTabs.layout(pl, pt, pl + sw, bottom - top);
+        // adjust for overlap
+        if (!mUseQuickControls) {
+            mNewTab.layout(pl + sw - mAddTabOverlap, pt,
+                    pl + sw + mButtonWidth - mAddTabOverlap, bottom - top);
+        }
+    }
+
+    public void onClick(View view) {
+        if (mNewTab == view) {
+            mUiController.openTabToHomePage();
+        } else if (mTabs.getSelectedTab() == view) {
+            if (mUseQuickControls) {
+                if (mUi.isTitleBarShowing() && !isLoading()) {
+                    mUi.stopEditingUrl();
+                    mUi.hideTitleBar();
+                } else {
+                    mUi.stopWebViewScrolling();
+                    mUi.editUrl(false, false);
+                }
+            } else if (mUi.isTitleBarShowing() && !isLoading()) {
+                mUi.stopEditingUrl();
+                mUi.hideTitleBar();
+            } else {
+                showUrlBar();
+            }
+        } else if (view instanceof TabView) {
+            final Tab tab = ((TabView) view).mTab;
+            int ix = mTabs.getChildIndex(view);
+            if (ix >= 0) {
+                mTabs.setSelectedTab(ix);
+                mUiController.switchToTab(tab);
+            }
+        }
+    }
+
+    private void showUrlBar() {
+        mUi.stopWebViewScrolling();
+        mUi.showTitleBar();
+    }
+
+    private TabView buildTabView(Tab tab) {
+        TabView tabview = new TabView(mActivity, tab);
+        mTabMap.put(tab, tabview);
+        tabview.setOnClickListener(this);
+        return tabview;
+    }
+
+    private static Bitmap getDrawableAsBitmap(Drawable drawable, int width, int height) {
+        Bitmap b = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+        Canvas c = new Canvas(b);
+        drawable.setBounds(0, 0, width, height);
+        drawable.draw(c);
+        c.setBitmap(null);
+        return b;
+    }
+
+    /**
+     * View used in the tab bar
+     */
+    class TabView extends LinearLayout implements OnClickListener {
+
+        Tab mTab;
+        View mTabContent;
+        TextView mTitle;
+        View mIncognito;
+        View mSnapshot;
+        ImageView mIconView;
+        ImageView mLock;
+        ImageView mClose;
+        boolean mSelected;
+        Path mPath;
+        Path mFocusPath;
+        int[] mWindowPos;
+
+        /**
+         * @param context
+         */
+        public TabView(Context context, Tab tab) {
+            super(context);
+            setWillNotDraw(false);
+            mPath = new Path();
+            mFocusPath = new Path();
+            mWindowPos = new int[2];
+            mTab = tab;
+            setGravity(Gravity.CENTER_VERTICAL);
+            setOrientation(LinearLayout.HORIZONTAL);
+            setPadding(mTabOverlap, 0, mTabSliceWidth, 0);
+            LayoutInflater inflater = LayoutInflater.from(getContext());
+            mTabContent = inflater.inflate(R.layout.tab_title, this, true);
+            mTitle = (TextView) mTabContent.findViewById(R.id.title);
+            mIconView = (ImageView) mTabContent.findViewById(R.id.favicon);
+            mLock = (ImageView) mTabContent.findViewById(R.id.lock);
+            mClose = (ImageView) mTabContent.findViewById(R.id.close);
+            mClose.setOnClickListener(this);
+            mIncognito = mTabContent.findViewById(R.id.incognito);
+            mSnapshot = mTabContent.findViewById(R.id.snapshot);
+            mSelected = false;
+            // update the status
+            updateFromTab();
+        }
+
+        @Override
+        public void onClick(View v) {
+            if (v == mClose) {
+                closeTab();
+            }
+        }
+
+        private void updateFromTab() {
+            String displayTitle = mTab.getTitle();
+            if (displayTitle == null) {
+                displayTitle = mTab.getUrl();
+            }
+            setDisplayTitle(displayTitle);
+            if (mTab.getFavicon() != null) {
+                setFavicon(mUi.getFaviconDrawable(mTab.getFavicon()));
+            }
+            updateTabIcons();
+        }
+
+        private void updateTabIcons() {
+            mIncognito.setVisibility(
+                    mTab.isPrivateBrowsingEnabled() ?
+                    View.VISIBLE : View.GONE);
+            mSnapshot.setVisibility(mTab.isSnapshot()
+                    ? View.VISIBLE : View.GONE);
+        }
+
+        @Override
+        public void setActivated(boolean selected) {
+            mSelected = selected;
+            mClose.setVisibility(mSelected ? View.VISIBLE : View.GONE);
+            mIconView.setVisibility(mSelected ? View.GONE : View.VISIBLE);
+            mTitle.setTextAppearance(mActivity, mSelected ?
+                    R.style.TabTitleSelected : R.style.TabTitleUnselected);
+            setHorizontalFadingEdgeEnabled(!mSelected);
+            super.setActivated(selected);
+            updateLayoutParams();
+            setFocusable(!selected);
+            postInvalidate();
+        }
+
+        public void updateLayoutParams() {
+            LayoutParams lp = (LinearLayout.LayoutParams) getLayoutParams();
+            lp.width = mTabWidth;
+            lp.height =  LayoutParams.MATCH_PARENT;
+            setLayoutParams(lp);
+        }
+
+        void setDisplayTitle(String title) {
+            mTitle.setText(title);
+        }
+
+        void setFavicon(Drawable d) {
+            mIconView.setImageDrawable(d);
+        }
+
+        void setLock(Drawable d) {
+            if (null == d) {
+                mLock.setVisibility(View.GONE);
+            } else {
+                mLock.setImageDrawable(d);
+                mLock.setVisibility(View.VISIBLE);
+            }
+        }
+
+        private void closeTab() {
+            if (mTab == mTabControl.getCurrentTab()) {
+                mUiController.closeCurrentTab();
+            } else {
+                mUiController.closeTab(mTab);
+            }
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int l, int t, int r, int b) {
+            super.onLayout(changed, l, t, r, b);
+            setTabPath(mPath, 0, 0, r - l, b - t);
+            setFocusPath(mFocusPath, 0, 0, r - l, b - t);
+        }
+
+        @Override
+        protected void dispatchDraw(Canvas canvas) {
+            if (mCurrentTextureWidth != mUi.getContentWidth() ||
+                    mCurrentTextureHeight != getHeight()) {
+                mCurrentTextureWidth = mUi.getContentWidth();
+                mCurrentTextureHeight = getHeight();
+
+                if (mCurrentTextureWidth > 0 && mCurrentTextureHeight > 0) {
+                    Bitmap activeTexture = getDrawableAsBitmap(mActiveDrawable,
+                            mCurrentTextureWidth, mCurrentTextureHeight);
+                    Bitmap inactiveTexture = getDrawableAsBitmap(mInactiveDrawable,
+                            mCurrentTextureWidth, mCurrentTextureHeight);
+
+                    mActiveShader = new BitmapShader(activeTexture,
+                            Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+                    mActiveShaderPaint.setShader(mActiveShader);
+
+                    mInactiveShader = new BitmapShader(inactiveTexture,
+                            Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
+                    mInactiveShaderPaint.setShader(mInactiveShader);
+                }
+            }
+            // add some monkey protection
+            if ((mActiveShader != null) && (mInactiveShader != null)) {
+                int state = canvas.save();
+                getLocationInWindow(mWindowPos);
+                Paint paint = mSelected ? mActiveShaderPaint : mInactiveShaderPaint;
+                drawClipped(canvas, paint, mPath, mWindowPos[0]);
+                canvas.restoreToCount(state);
+            }
+            super.dispatchDraw(canvas);
+        }
+
+        private void drawClipped(Canvas canvas, Paint paint, Path clipPath, int left) {
+            // TODO: We should change the matrix/shader only when needed
+            final Matrix matrix = mSelected ? mActiveMatrix : mInactiveMatrix;
+            matrix.setTranslate(-left, 0.0f);
+            (mSelected ? mActiveShader : mInactiveShader).setLocalMatrix(matrix);
+            canvas.drawPath(clipPath, paint);
+            if (isFocused()) {
+                canvas.drawPath(mFocusPath, mFocusPaint);
+            }
+        }
+
+        private void setTabPath(Path path, int l, int t, int r, int b) {
+            path.reset();
+            path.moveTo(l, b);
+            path.lineTo(l, t);
+            path.lineTo(r - mTabSliceWidth, t);
+            path.lineTo(r, b);
+            path.close();
+        }
+
+        private void setFocusPath(Path path, int l, int t, int r, int b) {
+            path.reset();
+            path.moveTo(l, b);
+            path.lineTo(l, t);
+            path.lineTo(r - mTabSliceWidth, t);
+            path.lineTo(r, b);
+        }
+
+    }
+
+    private void animateTabOut(final Tab tab, final TabView tv) {
+        ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 1.0f, 0.0f);
+        ObjectAnimator scaley = ObjectAnimator.ofFloat(tv, "scaleY", 1.0f, 0.0f);
+        ObjectAnimator alpha = ObjectAnimator.ofFloat(tv, "alpha", 1.0f, 0.0f);
+        AnimatorSet animator = new AnimatorSet();
+        animator.playTogether(scalex, scaley, alpha);
+        animator.setDuration(150);
+        animator.addListener(new AnimatorListener() {
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mTabs.removeTab(tv);
+                mTabMap.remove(tab);
+                mUi.onRemoveTabCompleted(tab);
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+            }
+
+        });
+        animator.start();
+    }
+
+    private void animateTabIn(final Tab tab, final TabView tv) {
+        ObjectAnimator scalex = ObjectAnimator.ofFloat(tv, "scaleX", 0.0f, 1.0f);
+        scalex.setDuration(150);
+        scalex.addListener(new AnimatorListener() {
+
+            @Override
+            public void onAnimationCancel(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animation) {
+                mUi.onAddTabCompleted(tab);
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animation) {
+            }
+
+            @Override
+            public void onAnimationStart(Animator animation) {
+                mTabs.addTab(tv);
+            }
+
+        });
+        scalex.start();
+    }
+
+    // TabChangeListener implementation
+
+    public void onSetActiveTab(Tab tab) {
+        mTabs.setSelectedTab(mTabControl.getTabPosition(tab));
+    }
+
+    public void onFavicon(Tab tab, Bitmap favicon) {
+        TabView tv = mTabMap.get(tab);
+        if (tv != null) {
+            tv.setFavicon(mUi.getFaviconDrawable(favicon));
+        }
+    }
+
+    public void onNewTab(Tab tab) {
+        TabView tv = buildTabView(tab);
+        animateTabIn(tab, tv);
+    }
+
+    public void onRemoveTab(Tab tab) {
+        TabView tv = mTabMap.get(tab);
+        if (tv != null) {
+            animateTabOut(tab, tv);
+        } else {
+            mTabMap.remove(tab);
+        }
+    }
+
+    public void onUrlAndTitle(Tab tab, String url, String title) {
+        TabView tv = mTabMap.get(tab);
+        if (tv != null) {
+            if (title != null) {
+                tv.setDisplayTitle(title);
+            } else if (url != null) {
+                tv.setDisplayTitle(UrlUtils.stripUrl(url));
+            }
+            tv.updateTabIcons();
+        }
+    }
+
+    private boolean isLoading() {
+        Tab tab = mTabControl.getCurrentTab();
+        if (tab != null) {
+            return tab.inPageLoad();
+        } else {
+            return false;
+        }
+    }
+
+}
diff --git a/src/com/android/browser/TabControl.java b/src/com/android/browser/TabControl.java
new file mode 100644
index 0000000..66736cb
--- /dev/null
+++ b/src/com/android/browser/TabControl.java
@@ -0,0 +1,721 @@
+/*
+ * Copyright (C) 2007 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.os.Bundle;
+import android.util.Log;
+
+import com.android.browser.reflect.ReflectHelper;
+
+import org.codeaurora.swe.WebView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Vector;
+
+class TabControl {
+    // Log Tag
+    private static final String LOGTAG = "TabControl";
+
+    // next Tab ID, starting at 1
+    private static long sNextId = 1;
+
+    private static final String POSITIONS = "positions";
+    private static final String CURRENT = "current";
+
+    public static interface OnThumbnailUpdatedListener {
+        void onThumbnailUpdated(Tab t);
+    }
+
+    // Maximum number of tabs.
+    private int mMaxTabs;
+    // Private array of WebViews that are used as tabs.
+    private ArrayList<Tab> mTabs;
+    // Queue of most recently viewed tabs.
+    private ArrayList<Tab> mTabQueue;
+    // Current position in mTabs.
+    private int mCurrentTab = -1;
+    // the main browser controller
+    private final Controller mController;
+
+    private OnThumbnailUpdatedListener mOnThumbnailUpdatedListener;
+
+    /**
+     * Construct a new TabControl object
+     */
+    TabControl(Controller controller) {
+        mController = controller;
+        mMaxTabs = mController.getMaxTabs();
+        mTabs = new ArrayList<Tab>(mMaxTabs);
+        mTabQueue = new ArrayList<Tab>(mMaxTabs);
+    }
+
+    synchronized static long getNextId() {
+        return sNextId++;
+    }
+
+    /**
+     * Return the current tab's main WebView. This will always return the main
+     * WebView for a given tab and not a subwindow.
+     * @return The current tab's WebView.
+     */
+    WebView getCurrentWebView() {
+        Tab t = getTab(mCurrentTab);
+        if (t == null) {
+            return null;
+        }
+        return t.getWebView();
+    }
+
+    /**
+     * Return the current tab's top-level WebView. This can return a subwindow
+     * if one exists.
+     * @return The top-level WebView of the current tab.
+     */
+    WebView getCurrentTopWebView() {
+        Tab t = getTab(mCurrentTab);
+        if (t == null) {
+            return null;
+        }
+        return t.getTopWindow();
+    }
+
+    /**
+     * Return the current tab's subwindow if it exists.
+     * @return The subwindow of the current tab or null if it doesn't exist.
+     */
+    WebView getCurrentSubWindow() {
+        Tab t = getTab(mCurrentTab);
+        if (t == null) {
+            return null;
+        }
+        return t.getSubWebView();
+    }
+
+    /**
+     * return the list of tabs
+     */
+    List<Tab> getTabs() {
+        return mTabs;
+    }
+
+    /**
+     * Return the tab at the specified position.
+     * @return The Tab for the specified position or null if the tab does not
+     *         exist.
+     */
+    Tab getTab(int position) {
+        if (position >= 0 && position < mTabs.size()) {
+            return mTabs.get(position);
+        }
+        return null;
+    }
+
+    /**
+     * Return the current tab.
+     * @return The current tab.
+     */
+    Tab getCurrentTab() {
+        return getTab(mCurrentTab);
+    }
+
+    /**
+     * Return the current tab position.
+     * @return The current tab position
+     */
+    int getCurrentPosition() {
+        return mCurrentTab;
+    }
+
+    /**
+     * Given a Tab, find it's position
+     * @param Tab to find
+     * @return position of Tab or -1 if not found
+     */
+    int getTabPosition(Tab tab) {
+        if (tab == null) {
+            return -1;
+        }
+        return mTabs.indexOf(tab);
+    }
+
+    boolean canCreateNewTab() {
+        return mMaxTabs > mTabs.size();
+    }
+
+    /**
+     * Returns true if there are any incognito tabs open.
+     * @return True when any incognito tabs are open, false otherwise.
+     */
+    boolean hasAnyOpenIncognitoTabs() {
+        for (Tab tab : mTabs) {
+            if (tab.getWebView() != null
+                    && tab.getWebView().isPrivateBrowsingEnabled()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void addPreloadedTab(Tab tab) {
+        for (Tab current : mTabs) {
+            if (current != null && current.getId() == tab.getId()) {
+                throw new IllegalStateException("Tab with id " + tab.getId() + " already exists: "
+                        + current.toString());
+            }
+        }
+        mTabs.add(tab);
+        tab.setController(mController);
+        mController.onSetWebView(tab, tab.getWebView());
+        tab.putInBackground();
+    }
+
+    /**
+     * Create a new tab.
+     * @return The newly createTab or null if we have reached the maximum
+     *         number of open tabs.
+     */
+    Tab createNewTab(boolean privateBrowsing) {
+        return createNewTab(null, privateBrowsing);
+    }
+
+    Tab createNewTab(Bundle state, boolean privateBrowsing) {
+        int size = mTabs.size();
+        // Return false if we have maxed out on tabs
+        if (!canCreateNewTab()) {
+            return null;
+        }
+
+        final WebView w = createNewWebView(privateBrowsing);
+
+        // Create a new tab and add it to the tab list
+        Tab t = new Tab(mController, w, state);
+        mTabs.add(t);
+        // Initially put the tab in the background.
+        t.putInBackground();
+        return t;
+    }
+
+    /**
+     * Create a new tab with default values for closeOnExit(false),
+     * appId(null), url(null), and privateBrowsing(false).
+     */
+    Tab createNewTab() {
+        return createNewTab(false);
+    }
+
+    SnapshotTab createSnapshotTab(long snapshotId) {
+        SnapshotTab t = new SnapshotTab(mController, snapshotId);
+        mTabs.add(t);
+        return t;
+    }
+
+    /**
+     * Remove the parent child relationships from all tabs.
+     */
+    void removeParentChildRelationShips() {
+        for (Tab tab : mTabs) {
+            tab.removeFromTree();
+        }
+    }
+
+    /**
+     * Remove the tab from the list. If the tab is the current tab shown, the
+     * last created tab will be shown.
+     * @param t The tab to be removed.
+     */
+    boolean removeTab(Tab t) {
+        if (t == null) {
+            return false;
+        }
+
+        // Grab the current tab before modifying the list.
+        Tab current = getCurrentTab();
+
+        // Remove t from our list of tabs.
+        mTabs.remove(t);
+
+        // Put the tab in the background only if it is the current one.
+        if (current == t) {
+            t.putInBackground();
+            mCurrentTab = -1;
+        } else {
+            // If a tab that is earlier in the list gets removed, the current
+            // index no longer points to the correct tab.
+            mCurrentTab = getTabPosition(current);
+        }
+
+        // destroy the tab
+        t.destroy();
+        // clear it's references to parent and children
+        t.removeFromTree();
+
+        // Remove it from the queue of viewed tabs.
+        mTabQueue.remove(t);
+        return true;
+    }
+
+    /**
+     * Destroy all the tabs and subwindows
+     */
+    void destroy() {
+        for (Tab t : mTabs) {
+            t.destroy();
+        }
+        mTabs.clear();
+        mTabQueue.clear();
+    }
+
+    /**
+     * Returns the number of tabs created.
+     * @return The number of tabs created.
+     */
+    int getTabCount() {
+        return mTabs.size();
+    }
+
+    /**
+     * save the tab state:
+     * current position
+     * position sorted array of tab ids
+     * for each tab id, save the tab state
+     * @param outState
+     * @param saveImages
+     */
+    void saveState(Bundle outState) {
+        final int numTabs = getTabCount();
+        if (numTabs == 0) {
+            return;
+        }
+        long[] ids = new long[numTabs];
+        int i = 0;
+        for (Tab tab : mTabs) {
+            Bundle tabState = tab.saveState();
+            if (tabState != null && tab.getWebView() != null
+                && tab.getWebView().isPrivateBrowsingEnabled() == false) {
+                ids[i++] = tab.getId();
+                String key = Long.toString(tab.getId());
+                if (outState.containsKey(key)) {
+                    // Dump the tab state for debugging purposes
+                    for (Tab dt : mTabs) {
+                        Log.e(LOGTAG, dt.toString());
+                    }
+                    throw new IllegalStateException(
+                            "Error saving state, duplicate tab ids!");
+                }
+                outState.putBundle(key, tabState);
+            } else {
+                ids[i++] = -1;
+                // Since we won't be restoring the thumbnail, delete it
+                tab.deleteThumbnail();
+            }
+        }
+        if (!outState.isEmpty()) {
+            outState.putLongArray(POSITIONS, ids);
+            Tab current = getCurrentTab();
+            long cid = -1;
+            if (current != null) {
+                cid = current.getId();
+            }
+            outState.putLong(CURRENT, cid);
+        }
+    }
+
+    /**
+     * Check if the state can be restored.  If the state can be restored, the
+     * current tab id is returned.  This can be passed to restoreState below
+     * in order to restore the correct tab.  Otherwise, -1 is returned and the
+     * state cannot be restored.
+     */
+    long canRestoreState(Bundle inState, boolean restoreIncognitoTabs) {
+        final long[] ids = (inState == null) ? null : inState.getLongArray(POSITIONS);
+        if (ids == null) {
+            return -1;
+        }
+        final long oldcurrent = inState.getLong(CURRENT);
+        long current = -1;
+        if (restoreIncognitoTabs || (hasState(oldcurrent, inState) && !isIncognito(oldcurrent, inState))) {
+            current = oldcurrent;
+        } else {
+            // pick first non incognito tab
+            for (long id : ids) {
+                if (hasState(id, inState) && !isIncognito(id, inState)) {
+                    current = id;
+                    break;
+                }
+            }
+        }
+        return current;
+    }
+
+    private boolean hasState(long id, Bundle state) {
+        if (id == -1) return false;
+        Bundle tab = state.getBundle(Long.toString(id));
+        return ((tab != null) && !tab.isEmpty());
+    }
+
+    private boolean isIncognito(long id, Bundle state) {
+        Bundle tabstate = state.getBundle(Long.toString(id));
+        if ((tabstate != null) && !tabstate.isEmpty()) {
+            return tabstate.getBoolean(Tab.INCOGNITO);
+        }
+        return false;
+    }
+
+    /**
+     * Restore the state of all the tabs.
+     * @param currentId The tab id to restore.
+     * @param inState The saved state of all the tabs.
+     * @param restoreIncognitoTabs Restoring private browsing tabs
+     * @param restoreAll All webviews get restored, not just the current tab
+     *        (this does not override handling of incognito tabs)
+     */
+    void restoreState(Bundle inState, long currentId,
+            boolean restoreIncognitoTabs, boolean restoreAll) {
+        if (currentId == -1) {
+            return;
+        }
+        long[] ids = inState.getLongArray(POSITIONS);
+        long maxId = -Long.MAX_VALUE;
+        HashMap<Long, Tab> tabMap = new HashMap<Long, Tab>();
+        for (long id : ids) {
+            if (id > maxId) {
+                maxId = id;
+            }
+            final String idkey = Long.toString(id);
+            Bundle state = inState.getBundle(idkey);
+            if (state == null || state.isEmpty()) {
+                // Skip tab
+                continue;
+            } else if (!restoreIncognitoTabs
+                    && state.getBoolean(Tab.INCOGNITO)) {
+                // ignore tab
+            } else if (id == currentId || restoreAll) {
+                Tab t = createNewTab(state, false);
+                if (t == null) {
+                    // We could "break" at this point, but we want
+                    // sNextId to be set correctly.
+                    continue;
+                }
+
+                // add for carrier homepage feature
+                // If the webview restore successfully, add javascript interface again.
+                WebView view = t.getWebView();
+                if (view != null) {
+                    Object[] params  = { new String("persist.env.c.browser.resource"),
+                                 new String("default")};
+                    Class[] type = new Class[] {String.class, String.class};
+                    String browserRes = (String)ReflectHelper.invokeStaticMethod(
+                        "android.os.SystemProperties","get",
+                        type, params);
+                    if ("ct".equals(browserRes)) {
+                        view.getSettings().setJavaScriptEnabled(true);
+                        if (mController.getActivity() instanceof BrowserActivity) {
+                            view.addJavascriptInterface(mController.getActivity(),
+                                    "default_homepage");
+                        }
+                    }
+                }
+
+                tabMap.put(id, t);
+                // Me must set the current tab before restoring the state
+                // so that all the client classes are set.
+                if (id == currentId) {
+                    setCurrentTab(t);
+                }
+            } else {
+                // Create a new tab and don't restore the state yet, add it
+                // to the tab list
+                Tab t = new Tab(mController, state);
+                tabMap.put(id, t);
+                mTabs.add(t);
+                // added the tab to the front as they are not current
+                mTabQueue.add(0, t);
+            }
+        }
+
+        // make sure that there is no id overlap between the restored
+        // and new tabs
+        sNextId = maxId + 1;
+
+        if (mCurrentTab == -1) {
+            if (getTabCount() > 0) {
+                setCurrentTab(getTab(0));
+            }
+        }
+        // restore parent/child relationships
+        for (long id : ids) {
+            final Tab tab = tabMap.get(id);
+            final Bundle b = inState.getBundle(Long.toString(id));
+            if ((b != null) && (tab != null)) {
+                final long parentId = b.getLong(Tab.PARENTTAB, -1);
+                if (parentId != -1) {
+                    final Tab parent = tabMap.get(parentId);
+                    if (parent != null) {
+                        parent.addChildTab(tab);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Free the memory in this order, 1) free the background tabs; 2) free the
+     * WebView cache;
+     */
+    void freeMemory() {
+        if (getTabCount() == 0) return;
+
+        // free the least frequently used background tabs
+        Vector<Tab> tabs = getHalfLeastUsedTabs(getCurrentTab());
+        if (tabs.size() > 0) {
+            Log.w(LOGTAG, "Free " + tabs.size() + " tabs in the browser");
+            for (Tab t : tabs) {
+                // store the WebView's state.
+                t.saveState();
+                // destroy the tab
+                t.destroy();
+            }
+            return;
+        }
+
+        // free the WebView's unused memory (this includes the cache)
+        Log.w(LOGTAG, "Free WebView's unused memory and cache");
+        WebView view = getCurrentWebView();
+        if (view != null) {
+            view.freeMemory();
+        }
+    }
+
+    private Vector<Tab> getHalfLeastUsedTabs(Tab current) {
+        Vector<Tab> tabsToGo = new Vector<Tab>();
+
+        // Don't do anything if we only have 1 tab or if the current tab is
+        // null.
+        if (getTabCount() == 1 || current == null) {
+            return tabsToGo;
+        }
+
+        if (mTabQueue.size() == 0) {
+            return tabsToGo;
+        }
+
+        // Rip through the queue starting at the beginning and tear down half of
+        // available tabs which are not the current tab or the parent of the
+        // current tab.
+        int openTabCount = 0;
+        for (Tab t : mTabQueue) {
+            if (t != null && t.getWebView() != null) {
+                openTabCount++;
+                if (t != current && t != current.getParent()) {
+                    tabsToGo.add(t);
+                }
+            }
+        }
+
+        openTabCount /= 2;
+        if (tabsToGo.size() > openTabCount) {
+            tabsToGo.setSize(openTabCount);
+        }
+
+        return tabsToGo;
+    }
+
+    Tab getLeastUsedTab(Tab current) {
+        if (getTabCount() == 1 || current == null) {
+            return null;
+        }
+        if (mTabQueue.size() == 0) {
+            return null;
+        }
+        // find a tab which is not the current tab or the parent of the
+        // current tab
+        for (Tab t : mTabQueue) {
+            if (t != null && t.getWebView() != null) {
+                if (t != current && t != current.getParent()) {
+                    return t;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Show the tab that contains the given WebView.
+     * @param view The WebView used to find the tab.
+     */
+    Tab getTabFromView(WebView view) {
+        for (Tab t : mTabs) {
+            if (t.getSubWebView() == view || t.getWebView() == view) {
+                return t;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the tab with the matching application id.
+     * @param id The application identifier.
+     */
+    Tab getTabFromAppId(String id) {
+        if (id == null) {
+            return null;
+        }
+        for (Tab t : mTabs) {
+            if (id.equals(t.getAppId())) {
+                return t;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Stop loading in all opened WebView including subWindows.
+     */
+    void stopAllLoading() {
+        for (Tab t : mTabs) {
+            final WebView webview = t.getWebView();
+            if (webview != null) {
+                webview.stopLoading();
+            }
+            final WebView subview = t.getSubWebView();
+            if (subview != null) {
+                subview.stopLoading();
+            }
+        }
+    }
+
+    // This method checks if a tab matches the given url.
+    private boolean tabMatchesUrl(Tab t, String url) {
+        return url.equals(t.getUrl()) || url.equals(t.getOriginalUrl());
+    }
+
+    /**
+     * Return the tab that matches the given url.
+     * @param url The url to search for.
+     */
+    Tab findTabWithUrl(String url) {
+        if (url == null) {
+            return null;
+        }
+        // Check the current tab first.
+        Tab currentTab = getCurrentTab();
+        if (currentTab != null && tabMatchesUrl(currentTab, url)) {
+            return currentTab;
+        }
+        // Now check all the rest.
+        for (Tab tab : mTabs) {
+            if (tabMatchesUrl(tab, url)) {
+                return tab;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Recreate the main WebView of the given tab.
+     */
+    void recreateWebView(Tab t) {
+        final WebView w = t.getWebView();
+        if (w != null) {
+            t.destroy();
+        }
+        // Create a new WebView. If this tab is the current tab, we need to put
+        // back all the clients so force it to be the current tab.
+        t.setWebView(createNewWebView(t.isPrivateBrowsingEnabled()), false);
+        if (getCurrentTab() == t) {
+            setCurrentTab(t, true);
+        }
+    }
+
+    /**
+     * Creates a new WebView and registers it with the global settings.
+     */
+    private WebView createNewWebView() {
+        return createNewWebView(false);
+    }
+
+    /**
+     * Creates a new WebView and registers it with the global settings.
+     * @param privateBrowsing When true, enables private browsing in the new
+     *        WebView.
+     */
+    private WebView createNewWebView(boolean privateBrowsing) {
+        return mController.getWebViewFactory().createWebView(privateBrowsing);
+    }
+
+    /**
+     * Put the current tab in the background and set newTab as the current tab.
+     * @param newTab The new tab. If newTab is null, the current tab is not
+     *               set.
+     */
+    boolean setCurrentTab(Tab newTab) {
+        return setCurrentTab(newTab, false);
+    }
+
+    /**
+     * If force is true, this method skips the check for newTab == current.
+     */
+    private boolean setCurrentTab(Tab newTab, boolean force) {
+        Tab current = getTab(mCurrentTab);
+        if (current == newTab && !force) {
+            return true;
+        }
+        if (current != null) {
+            current.putInBackground();
+            mCurrentTab = -1;
+        }
+        if (newTab == null) {
+            return false;
+        }
+
+        // Move the newTab to the end of the queue
+        int index = mTabQueue.indexOf(newTab);
+        if (index != -1) {
+            mTabQueue.remove(index);
+        }
+        mTabQueue.add(newTab);
+
+        // Display the new current tab
+        mCurrentTab = mTabs.indexOf(newTab);
+        WebView mainView = newTab.getWebView();
+        boolean needRestore = !newTab.isSnapshot() && (mainView == null);
+        if (needRestore) {
+            // Same work as in createNewTab() except don't do new Tab()
+            mainView = createNewWebView(newTab.isPrivateBrowsingEnabled());
+            newTab.setWebView(mainView);
+        }
+        newTab.putInForeground();
+        return true;
+    }
+
+    public void setOnThumbnailUpdatedListener(OnThumbnailUpdatedListener listener) {
+        mOnThumbnailUpdatedListener = listener;
+        for (Tab t : mTabs) {
+            WebView web = t.getWebView();
+            if (web != null) {
+                web.setPictureListener(listener != null ? t : null);
+            }
+        }
+    }
+
+    public OnThumbnailUpdatedListener getOnThumbnailUpdatedListener() {
+        return mOnThumbnailUpdatedListener;
+    }
+
+}
diff --git a/src/com/android/browser/TabScrollView.java b/src/com/android/browser/TabScrollView.java
new file mode 100644
index 0000000..1df88cc
--- /dev/null
+++ b/src/com/android/browser/TabScrollView.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2010 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 com.android.browser.R;
+import com.android.browser.TabBar.TabView;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.HorizontalScrollView;
+import android.widget.LinearLayout;
+
+/**
+ * custom view for displaying tabs in the tabbed title bar
+ */
+public class TabScrollView extends HorizontalScrollView {
+
+    private LinearLayout mContentView;
+    private int mSelected;
+    private int mAnimationDuration;
+    private int mTabOverlap;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public TabScrollView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public TabScrollView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /**
+     * @param context
+     */
+    public TabScrollView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        mAnimationDuration = ctx.getResources().getInteger(
+                R.integer.tab_animation_duration);
+        mTabOverlap = (int) ctx.getResources().getDimension(R.dimen.tab_overlap);
+        setHorizontalScrollBarEnabled(false);
+        setOverScrollMode(OVER_SCROLL_NEVER);
+        mContentView = new TabLayout(ctx);
+        mContentView.setOrientation(LinearLayout.HORIZONTAL);
+        mContentView.setLayoutParams(
+                new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
+        mContentView.setPadding(
+                (int) ctx.getResources().getDimension(R.dimen.tab_first_padding_left),
+                0, 0, 0);
+        addView(mContentView);
+        mSelected = -1;
+        // prevent ProGuard from removing the property methods
+        setScroll(getScroll());
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        ensureChildVisible(getSelectedTab());
+    }
+
+    // in case of a configuration change, adjust tab width
+    protected void updateLayout() {
+        final int count = mContentView.getChildCount();
+        for (int i = 0; i < count; i++) {
+            final TabView tv = (TabView) mContentView.getChildAt(i);
+            tv.updateLayoutParams();
+        }
+        ensureChildVisible(getSelectedTab());
+    }
+
+    void setSelectedTab(int position) {
+        View v = getSelectedTab();
+        if (v != null) {
+            v.setActivated(false);
+        }
+        mSelected = position;
+        v = getSelectedTab();
+        if (v != null) {
+            v.setActivated(true);
+        }
+        requestLayout();
+    }
+
+    int getChildIndex(View v) {
+        return mContentView.indexOfChild(v);
+    }
+
+    View getSelectedTab() {
+        if ((mSelected >= 0) && (mSelected < mContentView.getChildCount())) {
+            return mContentView.getChildAt(mSelected);
+        } else {
+            return null;
+        }
+    }
+
+    void clearTabs() {
+        mContentView.removeAllViews();
+    }
+
+    void addTab(View tab) {
+        mContentView.addView(tab);
+        tab.setActivated(false);
+    }
+
+    void removeTab(View tab) {
+        int ix = mContentView.indexOfChild(tab);
+        if (ix == mSelected) {
+            mSelected = -1;
+        } else if (ix < mSelected) {
+            mSelected--;
+        }
+        mContentView.removeView(tab);
+    }
+
+    private void ensureChildVisible(View child) {
+        if (child != null) {
+            int childl = child.getLeft();
+            int childr = childl + child.getWidth();
+            int viewl = getScrollX();
+            int viewr = viewl + getWidth();
+            if (childl < viewl) {
+                // need scrolling to left
+                animateScroll(childl);
+            } else if (childr > viewr) {
+                // need scrolling to right
+                animateScroll(childr - viewr + viewl);
+            }
+        }
+    }
+
+// TODO: These animations are broken and don't work correctly, removing for now
+//       as animateOut is actually causing issues
+//    private void animateIn(View tab) {
+//        ObjectAnimator animator = ObjectAnimator.ofInt(tab, "TranslationX", 500, 0);
+//        animator.setDuration(mAnimationDuration);
+//        animator.start();
+//    }
+//
+//    private void animateOut(final View tab) {
+//        ObjectAnimator animator = ObjectAnimator.ofInt(
+//                tab, "TranslationX", 0, getScrollX() - tab.getRight());
+//        animator.setDuration(mAnimationDuration);
+//        animator.addListener(new AnimatorListenerAdapter() {
+//            @Override
+//            public void onAnimationEnd(Animator animation) {
+//                mContentView.removeView(tab);
+//            }
+//        });
+//        animator.setInterpolator(new AccelerateInterpolator());
+//        animator.start();
+//    }
+
+    private void animateScroll(int newscroll) {
+        ObjectAnimator animator = ObjectAnimator.ofInt(this, "scroll", getScrollX(), newscroll);
+        animator.setDuration(mAnimationDuration);
+        animator.start();
+    }
+
+    /**
+     * required for animation
+     */
+    public void setScroll(int newscroll) {
+        scrollTo(newscroll, getScrollY());
+    }
+
+    /**
+     * required for animation
+     */
+    public int getScroll() {
+        return getScrollX();
+    }
+
+    @Override
+    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
+        super.onScrollChanged(l, t, oldl, oldt);
+
+        // TabViews base their drawing based on their absolute position within the
+        // window. When hardware accelerated, we need to recreate their display list
+        // when they scroll
+        if (isHardwareAccelerated()) {
+            int count = mContentView.getChildCount();
+            for (int i = 0; i < count; i++) {
+                mContentView.getChildAt(i).invalidate();
+            }
+        }
+    }
+
+    class TabLayout extends LinearLayout {
+
+        public TabLayout(Context context) {
+            super(context);
+            setChildrenDrawingOrderEnabled(true);
+        }
+
+        @Override
+        protected void onMeasure(int hspec, int vspec) {
+            super.onMeasure(hspec, vspec);
+            int w = getMeasuredWidth();
+            w -= Math.max(0, mContentView.getChildCount() - 1) * mTabOverlap;
+            setMeasuredDimension(w, getMeasuredHeight());
+        }
+
+        @Override
+        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+            super.onLayout(changed, left, top, right, bottom);
+            if (getChildCount() > 1) {
+                int nextLeft = getChildAt(0).getRight() - mTabOverlap;
+                for (int i = 1; i < getChildCount(); i++) {
+                    View tab = getChildAt(i);
+                    int w = tab.getRight() - tab.getLeft();
+                    tab.layout(nextLeft, tab.getTop(), nextLeft + w, tab.getBottom());
+                    nextLeft += w - mTabOverlap;
+                }
+            }
+        }
+
+        @Override
+        protected int getChildDrawingOrder(int count, int i) {
+            int next = -1;
+            if ((i == (count - 1)) && (mSelected >= 0) && (mSelected < count)) {
+                next = mSelected;
+            } else {
+                next = count - i - 1;
+                if (next <= mSelected && next > 0) {
+                    next--;
+                }
+            }
+            return next;
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/TitleBar.java b/src/com/android/browser/TitleBar.java
new file mode 100644
index 0000000..e33a05c
--- /dev/null
+++ b/src/com/android/browser/TitleBar.java
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2010 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.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import android.widget.FrameLayout;
+import android.widget.RelativeLayout;
+
+
+/**
+ * Base class for a title bar used by the browser.
+ */
+public class TitleBar extends RelativeLayout {
+
+    private static final int PROGRESS_MAX = 100;
+    private static final float ANIM_TITLEBAR_DECELERATE = 2.5f;
+
+    private UiController mUiController;
+    private BaseUi mBaseUi;
+    private FrameLayout mContentView;
+    private PageProgressView mProgress;
+    private AccessibilityManager mAccessibilityManager;
+
+    private AutologinBar mAutoLogin;
+    private NavigationBarBase mNavBar;
+    private boolean mUseQuickControls;
+    private SnapshotBar mSnapshotBar;
+
+    //state
+    private boolean mShowing;
+    private boolean mInLoad;
+    private boolean mSkipTitleBarAnimations;
+    private Animator mTitleBarAnimator;
+    private boolean mIsFixedTitleBar;
+
+    public TitleBar(Context context, UiController controller, BaseUi ui,
+            FrameLayout contentView) {
+        super(context, null);
+        mUiController = controller;
+        mBaseUi = ui;
+        mContentView = contentView;
+        mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
+        initLayout(context);
+        setFixedTitleBar();
+    }
+
+    private void initLayout(Context context) {
+        LayoutInflater factory = LayoutInflater.from(context);
+        factory.inflate(R.layout.title_bar, this);
+        mProgress = (PageProgressView) findViewById(R.id.progress);
+        mNavBar = (NavigationBarBase) findViewById(R.id.taburlbar);
+        mNavBar.setTitleBar(this);
+    }
+
+    private void inflateAutoLoginBar() {
+        if (mAutoLogin != null) {
+            return;
+        }
+
+        ViewStub stub = (ViewStub) findViewById(R.id.autologin_stub);
+        mAutoLogin = (AutologinBar) stub.inflate();
+        mAutoLogin.setTitleBar(this);
+    }
+
+    private void inflateSnapshotBar() {
+        if (mSnapshotBar != null) {
+            return;
+        }
+
+        ViewStub stub = (ViewStub) findViewById(R.id.snapshotbar_stub);
+        mSnapshotBar = (SnapshotBar) stub.inflate();
+        mSnapshotBar.setTitleBar(this);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        setFixedTitleBar();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        if (mIsFixedTitleBar) {
+            int margin = getMeasuredHeight() - calculateEmbeddedHeight();
+            mBaseUi.setContentViewMarginTop(-margin);
+        } else {
+            mBaseUi.setContentViewMarginTop(0);
+        }
+    }
+
+    private void setFixedTitleBar() {
+        boolean isFixed = !mUseQuickControls
+                && !getContext().getResources().getBoolean(R.bool.hide_title);
+        isFixed |= mAccessibilityManager.isEnabled();
+        // If getParent() returns null, we are initializing
+        ViewGroup parent = (ViewGroup)getParent();
+        if (mIsFixedTitleBar == isFixed && parent != null) return;
+        mIsFixedTitleBar = isFixed;
+        setSkipTitleBarAnimations(true);
+        show();
+        setSkipTitleBarAnimations(false);
+        if (parent != null) {
+            parent.removeView(this);
+        }
+        if (mIsFixedTitleBar) {
+            mBaseUi.addFixedTitleBar(this);
+        } else {
+            mContentView.addView(this, makeLayoutParams());
+            mBaseUi.setContentViewMarginTop(0);
+        }
+    }
+
+    public BaseUi getUi() {
+        return mBaseUi;
+    }
+
+    public UiController getUiController() {
+        return mUiController;
+    }
+
+    public void setUseQuickControls(boolean use) {
+        mUseQuickControls = use;
+        setFixedTitleBar();
+        if (use) {
+            this.setVisibility(View.GONE);
+        } else {
+            this.setVisibility(View.VISIBLE);
+        }
+    }
+
+    void setShowProgressOnly(boolean progress) {
+        if (progress && !wantsToBeVisible()) {
+            mNavBar.setVisibility(View.GONE);
+        } else {
+            mNavBar.setVisibility(View.VISIBLE);
+        }
+    }
+
+    void setSkipTitleBarAnimations(boolean skip) {
+        mSkipTitleBarAnimations = skip;
+    }
+
+    void setupTitleBarAnimator(Animator animator) {
+        Resources res = getContext().getResources();
+        int duration = res.getInteger(R.integer.titlebar_animation_duration);
+        animator.setInterpolator(new DecelerateInterpolator(
+                ANIM_TITLEBAR_DECELERATE));
+        animator.setDuration(duration);
+    }
+
+    void show() {
+        cancelTitleBarAnimation(false);
+        if (mUseQuickControls || mSkipTitleBarAnimations) {
+            this.setVisibility(View.VISIBLE);
+            this.setTranslationY(0);
+        } else {
+            int visibleHeight = getVisibleTitleHeight();
+            float startPos = (-getEmbeddedHeight() + visibleHeight);
+            if (getTranslationY() != 0) {
+                startPos = Math.max(startPos, getTranslationY());
+            }
+            mTitleBarAnimator = ObjectAnimator.ofFloat(this,
+                    "translationY",
+                    startPos, 0);
+            setupTitleBarAnimator(mTitleBarAnimator);
+            mTitleBarAnimator.start();
+        }
+        mShowing = true;
+    }
+
+    void hide() {
+        if (mUseQuickControls) {
+            this.setVisibility(View.GONE);
+        } else {
+            if (mIsFixedTitleBar) return;
+            if (!mSkipTitleBarAnimations) {
+                cancelTitleBarAnimation(false);
+                int visibleHeight = getVisibleTitleHeight();
+                mTitleBarAnimator = ObjectAnimator.ofFloat(this,
+                        "translationY", getTranslationY(),
+                        (-getEmbeddedHeight() + visibleHeight));
+                mTitleBarAnimator.addListener(mHideTileBarAnimatorListener);
+                setupTitleBarAnimator(mTitleBarAnimator);
+                mTitleBarAnimator.start();
+            } else {
+                onScrollChanged();
+            }
+        }
+        mShowing = false;
+    }
+
+    boolean isShowing() {
+        return mShowing;
+    }
+
+    void cancelTitleBarAnimation(boolean reset) {
+        if (mTitleBarAnimator != null) {
+            mTitleBarAnimator.cancel();
+            mTitleBarAnimator = null;
+        }
+        if (reset) {
+            setTranslationY(0);
+        }
+    }
+
+    private AnimatorListener mHideTileBarAnimatorListener = new AnimatorListener() {
+
+        @Override
+        public void onAnimationStart(Animator animation) {
+        }
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {
+        }
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            // update position
+            onScrollChanged();
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+        }
+    };
+
+    private int getVisibleTitleHeight() {
+        Tab tab = mBaseUi.getActiveTab();
+        WebView webview = tab != null ? tab.getWebView() : null;
+        return webview != null ? webview.getVisibleTitleHeight() : 0;
+    }
+
+    /**
+     * Update the progress, from 0 to 100.
+     */
+    public void setProgress(int newProgress) {
+        if (newProgress >= PROGRESS_MAX) {
+            mProgress.setProgress(PageProgressView.MAX_PROGRESS);
+            mProgress.setVisibility(View.GONE);
+            mInLoad = false;
+            mNavBar.onProgressStopped();
+            // check if needs to be hidden
+            if (!isEditingUrl() && !wantsToBeVisible()) {
+                if (mUseQuickControls) {
+                    hide();
+                } else {
+                    mBaseUi.showTitleBarForDuration();
+                }
+            }
+        } else {
+            if (!mInLoad) {
+                mProgress.setVisibility(View.VISIBLE);
+                mInLoad = true;
+                mNavBar.onProgressStarted();
+            }
+            mProgress.setProgress(newProgress * PageProgressView.MAX_PROGRESS
+                    / PROGRESS_MAX);
+            if (mUseQuickControls && !isEditingUrl()) {
+                setShowProgressOnly(true);
+            }
+            if (!mShowing) {
+                show();
+            }
+        }
+    }
+
+    public int getEmbeddedHeight() {
+        if (mUseQuickControls || mIsFixedTitleBar) return 0;
+        return calculateEmbeddedHeight();
+    }
+
+     public boolean isFixed() {
+        return mIsFixedTitleBar;
+    }
+
+    int calculateEmbeddedHeight() {
+        int height = mNavBar.getHeight();
+        if (mAutoLogin != null && mAutoLogin.getVisibility() == View.VISIBLE) {
+            height += mAutoLogin.getHeight();
+        }
+        return height;
+    }
+
+    public void updateAutoLogin(Tab tab, boolean animate) {
+        if (mAutoLogin == null) {
+            if  (tab.getDeviceAccountLogin() == null) {
+                return;
+            }
+            inflateAutoLoginBar();
+        }
+        mAutoLogin.updateAutoLogin(tab, animate);
+    }
+
+    public void showAutoLogin(boolean animate) {
+        if (mUseQuickControls) {
+            mBaseUi.showTitleBar();
+        }
+        if (mAutoLogin == null) {
+            inflateAutoLoginBar();
+        }
+        mAutoLogin.setVisibility(View.VISIBLE);
+        if (animate) {
+            mAutoLogin.startAnimation(AnimationUtils.loadAnimation(
+                    getContext(), R.anim.autologin_enter));
+        }
+    }
+
+    public void hideAutoLogin(boolean animate) {
+        if (mUseQuickControls) {
+            mBaseUi.hideTitleBar();
+            mAutoLogin.setVisibility(View.GONE);
+            mBaseUi.refreshWebView();
+        } else {
+            if (animate) {
+                Animation anim = AnimationUtils.loadAnimation(getContext(),
+                        R.anim.autologin_exit);
+                anim.setAnimationListener(new AnimationListener() {
+                    @Override
+                    public void onAnimationEnd(Animation a) {
+                        mAutoLogin.setVisibility(View.GONE);
+                        mBaseUi.refreshWebView();
+                    }
+
+                    @Override
+                    public void onAnimationStart(Animation a) {
+                    }
+
+                    @Override
+                    public void onAnimationRepeat(Animation a) {
+                    }
+                });
+                mAutoLogin.startAnimation(anim);
+            } else if (mAutoLogin.getAnimation() == null) {
+                mAutoLogin.setVisibility(View.GONE);
+                mBaseUi.refreshWebView();
+            }
+        }
+    }
+
+    public boolean wantsToBeVisible() {
+        return inAutoLogin()
+            || (mSnapshotBar != null && mSnapshotBar.getVisibility() == View.VISIBLE
+                    && mSnapshotBar.isAnimating());
+    }
+
+    private boolean inAutoLogin() {
+        return mAutoLogin != null && mAutoLogin.getVisibility() == View.VISIBLE;
+    }
+
+    public boolean isEditingUrl() {
+        return mNavBar.isEditingUrl();
+    }
+
+    public WebView getCurrentWebView() {
+        Tab t = mBaseUi.getActiveTab();
+        if (t != null) {
+            return t.getWebView();
+        } else {
+            return null;
+        }
+    }
+
+    public PageProgressView getProgressView() {
+        return mProgress;
+    }
+
+    public NavigationBarBase getNavigationBar() {
+        return mNavBar;
+    }
+
+    public boolean useQuickControls() {
+        return mUseQuickControls;
+    }
+
+    public boolean isInLoad() {
+        return mInLoad;
+    }
+
+    private ViewGroup.LayoutParams makeLayoutParams() {
+        return new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT,
+                LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    public View focusSearch(View focused, int dir) {
+        WebView web = getCurrentWebView();
+        if (FOCUS_DOWN == dir && hasFocus() && web != null
+                && web.hasFocusable() && web.getParent() != null) {
+            return web;
+        }
+        return super.focusSearch(focused, dir);
+    }
+
+    public void onTabDataChanged(Tab tab) {
+        if (mSnapshotBar != null) {
+            mSnapshotBar.onTabDataChanged(tab);
+        }
+
+        if (tab.isSnapshot()) {
+            inflateSnapshotBar();
+            mSnapshotBar.setVisibility(VISIBLE);
+            mNavBar.setVisibility(GONE);
+        } else {
+            if (mSnapshotBar != null) {
+                mSnapshotBar.setVisibility(GONE);
+            }
+            mNavBar.setVisibility(VISIBLE);
+        }
+    }
+
+    public void onScrollChanged() {
+        if (!mShowing && !mIsFixedTitleBar) {
+            setTranslationY(getVisibleTitleHeight() - getEmbeddedHeight());
+        }
+    }
+
+    public void onResume() {
+        setFixedTitleBar();
+    }
+
+}
diff --git a/src/com/android/browser/UI.java b/src/com/android/browser/UI.java
new file mode 100644
index 0000000..00dacdb
--- /dev/null
+++ b/src/com/android/browser/UI.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2010 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.res.Configuration;
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * UI interface definitions
+ */
+public interface UI {
+
+    public static enum ComboViews {
+        History,
+        Bookmarks,
+        Snapshots,
+    }
+
+    public void onPause();
+
+    public void onResume();
+
+    public void onDestroy();
+
+    public void onConfigurationChanged(Configuration config);
+
+    public boolean onBackKey();
+
+    public boolean onMenuKey();
+
+    public boolean needsRestoreAllTabs();
+
+    public void addTab(Tab tab);
+
+    public void removeTab(Tab tab);
+
+    public void setActiveTab(Tab tab);
+
+    public void updateTabs(List<Tab> tabs);
+
+    public void detachTab(Tab tab);
+
+    public void attachTab(Tab tab);
+
+    public void onSetWebView(Tab tab, WebView view);
+
+    public void createSubWindow(Tab tab, WebView subWebView);
+
+    public void attachSubWindow(View subContainer);
+
+    public void removeSubWindow(View subContainer);
+
+    public void onTabDataChanged(Tab tab);
+
+    public void onPageStopped(Tab tab);
+
+    public void onProgressChanged(Tab tab);
+
+    public void showActiveTabsPage();
+
+    public void removeActiveTabsPage();
+
+    public void showComboView(ComboViews startingView, Bundle extra);
+
+    public void showCustomView(View view, int requestedOrientation,
+            CustomViewCallback callback);
+
+    public void onHideCustomView();
+
+    public boolean isCustomViewShowing();
+
+    public boolean onPrepareOptionsMenu(Menu menu);
+
+    public void updateMenuState(Tab tab, Menu menu);
+
+    public void onOptionsMenuOpened();
+
+    public void onExtendedMenuOpened();
+
+    public boolean onOptionsItemSelected(MenuItem item);
+
+    public void onOptionsMenuClosed(boolean inLoad);
+
+    public void onExtendedMenuClosed(boolean inLoad);
+
+    public void onContextMenuCreated(Menu menu);
+
+    public void onContextMenuClosed(Menu menu, boolean inLoad);
+
+    public void onActionModeStarted(ActionMode mode);
+
+    public void onActionModeFinished(boolean inLoad);
+
+    public void setShouldShowErrorConsole(Tab tab, boolean show);
+
+    // returns if the web page is clear of any overlays (not including sub windows)
+    public boolean isWebShowing();
+
+    public void showWeb(boolean animate);
+
+    Bitmap getDefaultVideoPoster();
+
+    View getVideoLoadingProgressView();
+
+    void bookmarkedStatusHasChanged(Tab tab);
+
+    void showMaxTabsWarning();
+
+    void editUrl(boolean clearInput, boolean forceIME);
+
+    boolean isEditingUrl();
+
+    boolean dispatchKey(int code, KeyEvent event);
+
+    void showAutoLogin(Tab tab);
+
+    void hideAutoLogin(Tab tab);
+
+    void setFullscreen(boolean enabled);
+
+    void setUseQuickControls(boolean enabled);
+
+    public boolean shouldCaptureThumbnails();
+
+    boolean blockFocusAnimations();
+
+    void onVoiceResult(String result);
+}
diff --git a/src/com/android/browser/UiController.java b/src/com/android/browser/UiController.java
new file mode 100644
index 0000000..36ee452
--- /dev/null
+++ b/src/com/android/browser/UiController.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2010 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.view.Menu;
+import android.view.MenuItem;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.UI.ComboViews;
+
+import java.util.List;
+
+
+/**
+ * UI aspect of the controller
+ */
+public interface UiController {
+
+    UI getUi();
+
+    WebView getCurrentWebView();
+
+    WebView getCurrentTopWebView();
+
+    Tab getCurrentTab();
+
+    TabControl getTabControl();
+
+    List<Tab> getTabs();
+
+    Tab openTabToHomePage();
+
+    Tab openIncognitoTab();
+
+    Tab openTab(String url, boolean incognito, boolean setActive,
+            boolean useCurrent);
+
+    void setActiveTab(Tab tab);
+
+    boolean switchToTab(Tab tab);
+
+    void closeCurrentTab();
+
+    void closeTab(Tab tab);
+
+    void closeOtherTabs();
+
+    void stopLoading();
+
+    Intent createBookmarkCurrentPageIntent(boolean canBeAnEdit);
+
+    void bookmarksOrHistoryPicker(ComboViews startView);
+
+    void bookmarkCurrentPage();
+
+    void editUrl();
+
+    void handleNewIntent(Intent intent);
+
+    boolean shouldShowErrorConsole();
+
+    void hideCustomView();
+
+    void attachSubWindow(Tab tab);
+
+    void removeSubWindow(Tab tab);
+
+    boolean isInCustomActionMode();
+
+    void endActionMode();
+
+    void shareCurrentPage();
+
+    void updateMenuState(Tab tab, Menu menu);
+
+    boolean onOptionsItemSelected(MenuItem item);
+
+    SnapshotTab createNewSnapshotTab(long snapshotId, boolean setActive);
+
+    void loadUrl(Tab tab, String url);
+
+    void setBlockEvents(boolean block);
+
+    Activity getActivity();
+
+    void showPageInfo();
+
+    void openPreferences();
+
+    void findOnPage();
+
+    void toggleUserAgent();
+
+    BrowserSettings getSettings();
+
+    boolean supportsVoice();
+
+    void startVoiceRecognizer();
+
+}
diff --git a/src/com/android/browser/UploadHandler.java b/src/com/android/browser/UploadHandler.java
new file mode 100644
index 0000000..8dec49c
--- /dev/null
+++ b/src/com/android/browser/UploadHandler.java
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2010 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.ActivityNotFoundException;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.webkit.ValueCallback;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.File;
+import java.util.Vector;
+
+/**
+ * Handle the file upload callbacks from WebView here
+ */
+public class UploadHandler {
+
+    /*
+     * The Object used to inform the WebView of the file to upload.
+     */
+    private ValueCallback<Uri> mUploadMessage;
+    private String mCameraFilePath;
+
+    private boolean mHandled;
+    private boolean mCaughtActivityNotFoundException;
+
+    private Controller mController;
+
+    public UploadHandler(Controller controller) {
+        mController = controller;
+    }
+
+    String getFilePath() {
+        return mCameraFilePath;
+    }
+
+    boolean handled() {
+        return mHandled;
+    }
+
+    private boolean isDrmFileUpload(Uri uri) {
+        if (uri == null) return false;
+
+        String path = null;
+        String scheme = uri.getScheme();
+        if ("content".equals(scheme)) {
+            String[] proj = null;
+            if (uri.toString().contains("/images/")) {
+                proj = new String[]{MediaStore.Images.Media.DATA};
+            } else if (uri.toString().contains("/audio/")) {
+                proj = new String[]{MediaStore.Audio.Media.DATA};
+            } else if (uri.toString().contains("/video/")) {
+                proj = new String[]{MediaStore.Video.Media.DATA};
+            }
+            Cursor cursor = mController.getActivity().managedQuery(uri, proj, null, null, null);
+            if (cursor != null && cursor.moveToFirst() && proj != null) {
+                path = cursor.getString(0);
+            }
+        } else if ("file".equals(scheme)) {
+            path = uri.getPath();
+        }
+        if (path != null) {
+            if (path.endsWith(".fl") || path.endsWith(".dm")
+                    || path.endsWith(".dcf") || path.endsWith(".dr") || path.endsWith(".dd")) {
+                Toast.makeText(mController.getContext(), R.string.drm_file_unsupported,
+                        Toast.LENGTH_LONG).show();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void onResult(int resultCode, Intent intent) {
+
+        if (resultCode == Activity.RESULT_CANCELED && mCaughtActivityNotFoundException) {
+            // Couldn't resolve an activity, we are going to try again so skip
+            // this result.
+            mCaughtActivityNotFoundException = false;
+            return;
+        }
+
+        Uri result = intent == null || resultCode != Activity.RESULT_OK ? null
+                : intent.getData();
+
+        // As we ask the camera to save the result of the user taking
+        // a picture, the camera application does not return anything other
+        // than RESULT_OK. So we need to check whether the file we expected
+        // was written to disk in the in the case that we
+        // did not get an intent returned but did get a RESULT_OK. If it was,
+        // we assume that this result has came back from the camera.
+        if (result == null && intent == null && resultCode == Activity.RESULT_OK) {
+            File cameraFile = new File(mCameraFilePath);
+            if (cameraFile.exists()) {
+                result = Uri.fromFile(cameraFile);
+                // Broadcast to the media scanner that we have a new photo
+                // so it will be added into the gallery for the user.
+                mController.getActivity().sendBroadcast(
+                        new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, result));
+            }
+        }
+
+        // add unsupport uploading drm file feature for carrier.
+        Object[] params  = {new String("persist.env.browser.drmupload"),
+                            Boolean.valueOf(false)};
+        Class[] type = new Class[] {String.class, boolean.class};
+        Boolean drmUpload = (Boolean) ReflectHelper.invokeStaticMethod(
+                      "android.os.SystemProperties", "getBoolean", type, params);
+        if (drmUpload && isDrmFileUpload(result)) {
+            mUploadMessage.onReceiveValue(null);
+        } else {
+            mUploadMessage.onReceiveValue(result);
+        }
+
+        mHandled = true;
+        mCaughtActivityNotFoundException = false;
+    }
+
+    void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
+
+        final String imageMimeType = "image/*";
+        final String videoMimeType = "video/*";
+        final String audioMimeType = "audio/*";
+        final String mediaSourceKey = "capture";
+        final String mediaSourceValueCamera = "camera";
+        final String mediaSourceValueFileSystem = "filesystem";
+        final String mediaSourceValueCamcorder = "camcorder";
+        final String mediaSourceValueMicrophone = "microphone";
+
+        // According to the spec, media source can be 'filesystem' or 'camera' or 'camcorder'
+        // or 'microphone' and the default value should be 'filesystem'.
+        String mediaSource = mediaSourceValueFileSystem;
+
+        if (mUploadMessage != null) {
+            // Already a file picker operation in progress.
+            return;
+        }
+
+        mUploadMessage = uploadMsg;
+
+        // Parse the accept type.
+        String params[] = acceptType.split(";");
+        String mimeType = params[0];
+
+        if (capture.length() > 0) {
+            mediaSource = capture;
+        }
+
+        if (capture.equals(mediaSourceValueFileSystem)) {
+            // To maintain backwards compatibility with the previous implementation
+            // of the media capture API, if the value of the 'capture' attribute is
+            // "filesystem", we should examine the accept-type for a MIME type that
+            // may specify a different capture value.
+            for (String p : params) {
+                String[] keyValue = p.split("=");
+                if (keyValue.length == 2) {
+                    // Process key=value parameters.
+                    if (mediaSourceKey.equals(keyValue[0])) {
+                        mediaSource = keyValue[1];
+                    }
+                }
+            }
+        }
+
+        //Ensure it is not still set from a previous upload.
+        mCameraFilePath = null;
+
+        if (mimeType.equals(imageMimeType)) {
+            if (mediaSource.equals(mediaSourceValueCamera)) {
+                // Specified 'image/*' and requested the camera, so go ahead and launch the
+                // camera directly.
+                startActivity(createCameraIntent());
+                return;
+            } else {
+                // Specified just 'image/*', capture=filesystem, or an invalid capture parameter.
+                // In all these cases we show a traditional picker filetered on accept type
+                // so launch an intent for both the Camera and image/* OPENABLE.
+                Intent chooser = createChooserIntent(createCameraIntent());
+                chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(imageMimeType));
+                startActivity(chooser);
+                return;
+            }
+        } else if (mimeType.equals(videoMimeType)) {
+            if (mediaSource.equals(mediaSourceValueCamcorder)) {
+                // Specified 'video/*' and requested the camcorder, so go ahead and launch the
+                // camcorder directly.
+                startActivity(createCamcorderIntent());
+                return;
+           } else {
+                // Specified just 'video/*', capture=filesystem or an invalid capture parameter.
+                // In all these cases we show an intent for the traditional file picker, filtered
+                // on accept type so launch an intent for both camcorder and video/* OPENABLE.
+                Intent chooser = createChooserIntent(createCamcorderIntent());
+                chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(videoMimeType));
+                startActivity(chooser);
+                return;
+            }
+        } else if (mimeType.equals(audioMimeType)) {
+            if (mediaSource.equals(mediaSourceValueMicrophone)) {
+                // Specified 'audio/*' and requested microphone, so go ahead and launch the sound
+                // recorder.
+                startActivity(createSoundRecorderIntent());
+                return;
+            } else {
+                // Specified just 'audio/*',  capture=filesystem of an invalid capture parameter.
+                // In all these cases so go ahead and launch an intent for both the sound
+                // recorder and audio/* OPENABLE.
+                Intent chooser = createChooserIntent(createSoundRecorderIntent());
+                chooser.putExtra(Intent.EXTRA_INTENT, createOpenableIntent(audioMimeType));
+                startActivity(chooser);
+                return;
+            }
+        }
+
+        // No special handling based on the accept type was necessary, so trigger the default
+        // file upload chooser.
+        startActivity(createDefaultOpenableIntent());
+    }
+
+    private void startActivity(Intent intent) {
+        try {
+            mController.getActivity().startActivityForResult(intent, Controller.FILE_SELECTED);
+        } catch (ActivityNotFoundException e) {
+            // No installed app was able to handle the intent that
+            // we sent, so fallback to the default file upload control.
+            try {
+                mCaughtActivityNotFoundException = true;
+                mController.getActivity().startActivityForResult(createDefaultOpenableIntent(),
+                        Controller.FILE_SELECTED);
+            } catch (ActivityNotFoundException e2) {
+                // Nothing can return us a file, so file upload is effectively disabled.
+                Toast.makeText(mController.getActivity(), R.string.uploads_disabled,
+                        Toast.LENGTH_LONG).show();
+            }
+        }
+    }
+
+    private Intent createDefaultOpenableIntent() {
+        // Create and return a chooser with the default OPENABLE
+        // actions including the camera, camcorder and sound
+        // recorder where available.
+        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+        i.addCategory(Intent.CATEGORY_OPENABLE);
+        i.setType("*/*");
+
+        Intent chooser = createChooserIntent(createCameraIntent(), createCamcorderIntent(),
+                createSoundRecorderIntent());
+        chooser.putExtra(Intent.EXTRA_INTENT, i);
+        return chooser;
+    }
+
+    private Intent createChooserIntent(Intent... intents) {
+        Intent chooser = new Intent(Intent.ACTION_CHOOSER);
+        chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents);
+        chooser.putExtra(Intent.EXTRA_TITLE,
+                mController.getActivity().getResources()
+                        .getString(R.string.choose_upload));
+        return chooser;
+    }
+
+    private Intent createOpenableIntent(String type) {
+        Intent i = new Intent(Intent.ACTION_GET_CONTENT);
+        i.addCategory(Intent.CATEGORY_OPENABLE);
+        i.setType(type);
+        return i;
+    }
+
+    private Intent createCameraIntent() {
+        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+        File externalDataDir = Environment.getExternalStoragePublicDirectory(
+                Environment.DIRECTORY_DCIM);
+        File cameraDataDir = new File(externalDataDir.getAbsolutePath() +
+                File.separator + "browser-photos");
+        cameraDataDir.mkdirs();
+        mCameraFilePath = cameraDataDir.getAbsolutePath() + File.separator +
+                System.currentTimeMillis() + ".jpg";
+        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(new File(mCameraFilePath)));
+        return cameraIntent;
+    }
+
+    private Intent createCamcorderIntent() {
+        return new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
+    }
+
+    private Intent createSoundRecorderIntent() {
+        return new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
+    }
+
+}
diff --git a/src/com/android/browser/UrlBarAutoShowManager.java b/src/com/android/browser/UrlBarAutoShowManager.java
new file mode 100644
index 0000000..4ef1765
--- /dev/null
+++ b/src/com/android/browser/UrlBarAutoShowManager.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 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.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnTouchListener;
+import android.view.ViewConfiguration;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserWebView.OnScrollChangedListener;
+
+/**
+ * Helper class to manage when to show the URL bar based off of touch
+ * input, and when to begin the hide timer.
+ */
+public class UrlBarAutoShowManager implements OnTouchListener,
+        OnScrollChangedListener {
+
+    private static float V_TRIGGER_ANGLE = .9f;
+    private static long SCROLL_TIMEOUT_DURATION = 150;
+    private static long IGNORE_INTERVAL = 250;
+
+    private BrowserWebView mTarget;
+    private BaseUi mUi;
+
+    private int mSlop;
+
+    private float mStartTouchX;
+    private float mStartTouchY;
+    private boolean mIsTracking;
+    private boolean mHasTriggered;
+    private long mLastScrollTime;
+    private long mTriggeredTime;
+    private boolean mIsScrolling;
+
+    public UrlBarAutoShowManager(BaseUi ui) {
+        mUi = ui;
+        ViewConfiguration config = ViewConfiguration.get(mUi.getActivity());
+        mSlop = config.getScaledTouchSlop() * 2;
+    }
+
+    public void setTarget(BrowserWebView v) {
+        if (mTarget == v) return;
+
+        if (mTarget != null) {
+            mTarget.setOnTouchListener(null);
+            mTarget.setOnScrollChangedListener(null);
+        }
+        mTarget = v;
+        if (mTarget != null) {
+            mTarget.setOnTouchListener(this);
+            mTarget.setOnScrollChangedListener(this);
+        }
+    }
+
+    @Override
+    public void onScrollChanged(int l, int t, int oldl, int oldt) {
+        mLastScrollTime = SystemClock.uptimeMillis();
+        mIsScrolling = true;
+        if (t != 0) {
+            // If it is showing, extend it
+            if (mUi.isTitleBarShowing()) {
+                long remaining = mLastScrollTime - mTriggeredTime;
+                remaining = Math.max(BaseUi.HIDE_TITLEBAR_DELAY - remaining,
+                        SCROLL_TIMEOUT_DURATION);
+                mUi.showTitleBarForDuration(remaining);
+            }
+        } else {
+            mUi.suggestHideTitleBar();
+        }
+    }
+
+    void stopTracking() {
+        if (mIsTracking) {
+            mIsTracking = false;
+            mIsScrolling = false;
+            if (mUi.isTitleBarShowing()) {
+                mUi.showTitleBarForDuration();
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        if (event.getPointerCount() > 1) {
+            stopTracking();
+        }
+        switch (event.getAction()) {
+        case MotionEvent.ACTION_DOWN:
+            if (!mIsTracking && event.getPointerCount() == 1) {
+                long sinceLastScroll =
+                        SystemClock.uptimeMillis() - mLastScrollTime;
+                if (sinceLastScroll < IGNORE_INTERVAL) {
+                    break;
+                }
+                mStartTouchY = event.getY();
+                mStartTouchX = event.getX();
+                mIsTracking = true;
+                mHasTriggered = false;
+            }
+            break;
+        case MotionEvent.ACTION_MOVE:
+            if (mIsTracking && !mHasTriggered) {
+                WebView web = (WebView) v;
+                float dy = event.getY() - mStartTouchY;
+                float ady = Math.abs(dy);
+                float adx = Math.abs(event.getX() - mStartTouchX);
+                if (ady > mSlop) {
+                    mHasTriggered = true;
+                    float angle = (float) Math.atan2(ady, adx);
+                    if (dy > mSlop && angle > V_TRIGGER_ANGLE
+                            && !mUi.isTitleBarShowing()
+                            && (web.getVisibleTitleHeight() == 0
+                            || (!mIsScrolling && web.getScrollY() > 0))) {
+                        mTriggeredTime = SystemClock.uptimeMillis();
+                        mUi.showTitleBar();
+                    }
+                }
+            }
+            break;
+        case MotionEvent.ACTION_CANCEL:
+        case MotionEvent.ACTION_UP:
+            stopTracking();
+            break;
+        }
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/UrlHandler.java b/src/com/android/browser/UrlHandler.java
new file mode 100755
index 0000000..783f11a
--- /dev/null
+++ b/src/com/android/browser/UrlHandler.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2010 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.ActivityNotFoundException;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.provider.Browser;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.regex.Matcher;
+
+import org.codeaurora.swe.WebView;
+
+public class UrlHandler {
+
+    private final static String TAG = "UrlHandler";
+
+    // Use in overrideUrlLoading
+    /* package */ final static String SCHEME_WTAI = "wtai://wp/";
+    /* package */ final static String SCHEME_WTAI_MC = "wtai://wp/mc;";
+    /* package */ final static String SCHEME_WTAI_SD = "wtai://wp/sd;";
+    /* package */ final static String SCHEME_WTAI_AP = "wtai://wp/ap;";
+
+    Controller mController;
+    Activity mActivity;
+
+    public UrlHandler(Controller controller) {
+        mController = controller;
+        mActivity = mController.getActivity();
+    }
+
+    boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url) {
+        if (view.isPrivateBrowsingEnabled()) {
+            // Don't allow urls to leave the browser app when in
+            // private browsing mode
+            return false;
+        }
+
+        if (url.startsWith(SCHEME_WTAI)) {
+            // wtai://wp/mc;number
+            // number=string(phone-number)
+            if (url.startsWith(SCHEME_WTAI_MC)) {
+                Intent intent = new Intent(Intent.ACTION_VIEW,
+                        Uri.parse(WebView.SCHEME_TEL +
+                        url.substring(SCHEME_WTAI_MC.length())));
+                mActivity.startActivity(intent);
+                // before leaving BrowserActivity, close the empty child tab.
+                // If a new tab is created through JavaScript open to load this
+                // url, we would like to close it as we will load this url in a
+                // different Activity.
+                mController.closeEmptyTab();
+                return true;
+            }
+            // wtai://wp/sd;dtmf
+            // dtmf=string(dialstring)
+            if (url.startsWith(SCHEME_WTAI_SD)) {
+                // TODO: only send when there is active voice connection
+                return false;
+            }
+            // wtai://wp/ap;number;name
+            // number=string(phone-number)
+            // name=string
+            if (url.startsWith(SCHEME_WTAI_AP)) {
+                // TODO
+                return false;
+            }
+        }
+
+        // The "about:" schemes are internal to the browser; don't want these to
+        // be dispatched to other apps.
+        if (url.startsWith("about:")) {
+            return false;
+        }
+
+        if (url.startsWith("ae://") && url.endsWith("add-fav")) {
+            mController.startAddMyNavigation(url);
+            return true;
+        }
+
+        // add for carrier wap2estore feature
+        Object[] params  = {new String("persist.env.browser.wap2estore"),
+                            Boolean.valueOf(false)};
+        Class[] type = new Class[] {String.class, boolean.class};
+        Boolean wap2estore = (Boolean)ReflectHelper.invokeStaticMethod(
+                      "android.os.SystemProperties", "getBoolean", type, params);
+        if (wap2estore && isEstoreTypeUrl(url)) {
+            handleEstoreTypeUrl(url);
+            return true;
+        }
+
+        if (startActivityForUrl(tab, url)) {
+            return true;
+        }
+
+        if (handleMenuClick(tab, url)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean isEstoreTypeUrl(String url) {
+        String utf8Url = null;
+        try {
+            utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "err " + e);
+        }
+        if (utf8Url != null && utf8Url.startsWith("estore:")) {
+            return true;
+        }
+        return false;
+    }
+
+    private void handleEstoreTypeUrl(String url) {
+        String utf8Url = null, finalUrl = null;
+        try {
+            utf8Url = new String(url.getBytes("UTF-8"), "UTF-8");
+        } catch (UnsupportedEncodingException e) {
+            Log.e(TAG, "err " + e);
+        }
+        if (utf8Url != null) {
+            finalUrl = utf8Url;
+        } else {
+            finalUrl = url;
+        }
+        if (finalUrl.replaceFirst("estore:", "").length() > 256) {
+            Toast.makeText(mActivity, R.string.estore_url_warning, Toast.LENGTH_LONG).show();
+            return;
+        }
+        Intent intent = new Intent(Intent.ACTION_VIEW);
+        intent.setData(Uri.parse(finalUrl));
+        try {
+            mActivity.startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            String downloadUrl = mActivity.getResources().getString(R.string.estore_homepage);
+            mController.loadUrl(mController.getCurrentTab(), downloadUrl);
+            Toast.makeText(mActivity, R.string.download_estore_app, Toast.LENGTH_LONG).show();
+        }
+    }
+
+    boolean startActivityForUrl(Tab tab, String url) {
+      Intent intent;
+      // perform generic parsing of the URI to turn it into an Intent.
+      try {
+          intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME);
+      } catch (URISyntaxException ex) {
+          Log.w("Browser", "Bad URI " + url + ": " + ex.getMessage());
+          return false;
+      }
+
+      // check whether the intent can be resolved. If not, we will see
+      // whether we can download it from the Market.
+      if (mActivity.getPackageManager().resolveActivity(intent, 0) == null) {
+          String packagename = intent.getPackage();
+          if (packagename != null) {
+              intent = new Intent(Intent.ACTION_VIEW, Uri
+                      .parse("market://search?q=pname:" + packagename));
+              intent.addCategory(Intent.CATEGORY_BROWSABLE);
+              mActivity.startActivity(intent);
+              // before leaving BrowserActivity, close the empty child tab.
+              // If a new tab is created through JavaScript open to load this
+              // url, we would like to close it as we will load this url in a
+              // different Activity.
+              mController.closeEmptyTab();
+              return true;
+          } else {
+              return false;
+          }
+      }
+
+      // sanitize the Intent, ensuring web pages can not bypass browser
+      // security (only access to BROWSABLE activities).
+      intent.addCategory(Intent.CATEGORY_BROWSABLE);
+      intent.setComponent(null);
+      // Re-use the existing tab if the intent comes back to us
+      if (tab != null) {
+          if (tab.getAppId() == null) {
+              tab.setAppId(mActivity.getPackageName() + "-" + tab.getId());
+          }
+          intent.putExtra(Browser.EXTRA_APPLICATION_ID, tab.getAppId());
+      }
+      // Make sure webkit can handle it internally before checking for specialized
+      // handlers. If webkit can't handle it internally, we need to call
+      // startActivityIfNeeded
+      Matcher m = UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url);
+      if (m.matches() && !isSpecializedHandlerAvailable(intent)) {
+          return false;
+      }
+      try {
+          intent.putExtra(BrowserActivity.EXTRA_DISABLE_URL_OVERRIDE, true);
+          if (mActivity.startActivityIfNeeded(intent, -1)) {
+              // before leaving BrowserActivity, close the empty child tab.
+              // If a new tab is created through JavaScript open to load this
+              // url, we would like to close it as we will load this url in a
+              // different Activity.
+              mController.closeEmptyTab();
+              return true;
+          }
+      } catch (ActivityNotFoundException ex) {
+          // ignore the error. If no application can handle the URL,
+          // eg about:blank, assume the browser can handle it.
+      }
+
+      return false;
+    }
+
+    /**
+     * Search for intent handlers that are specific to this URL
+     * aka, specialized apps like google maps or youtube
+     */
+    private boolean isSpecializedHandlerAvailable(Intent intent) {
+        PackageManager pm = mActivity.getPackageManager();
+          List<ResolveInfo> handlers = pm.queryIntentActivities(intent,
+                  PackageManager.GET_RESOLVED_FILTER);
+          if (handlers == null || handlers.size() == 0) {
+              return false;
+          }
+          for (ResolveInfo resolveInfo : handlers) {
+              IntentFilter filter = resolveInfo.filter;
+              if (filter == null) {
+                  // No intent filter matches this intent?
+                  // Error on the side of staying in the browser, ignore
+                  continue;
+              }
+              if (filter.countDataAuthorities() == 0 && filter.countDataPaths() == 0) {
+                  // Generic handler, skip
+                  continue;
+              }
+              return true;
+          }
+          return false;
+    }
+
+    // In case a physical keyboard is attached, handle clicks with the menu key
+    // depressed by opening in a new tab
+    boolean handleMenuClick(Tab tab, String url) {
+        if (mController.isMenuDown()) {
+            mController.openTab(url,
+                    (tab != null) && tab.isPrivateBrowsingEnabled(),
+                    !BrowserSettings.getInstance().openInBackground(), true);
+            mActivity.closeOptionsMenu();
+            return true;
+        }
+
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/UrlInputView.java b/src/com/android/browser/UrlInputView.java
new file mode 100644
index 0000000..1359000
--- /dev/null
+++ b/src/com/android/browser/UrlInputView.java
@@ -0,0 +1,362 @@
+/*
+ * Copyright (C) 2010 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.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Patterns;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AutoCompleteTextView;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.browser.SuggestionsAdapter.CompletionListener;
+import com.android.browser.SuggestionsAdapter.SuggestItem;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.search.SearchEngine;
+import com.android.browser.search.SearchEngineInfo;
+import com.android.browser.search.SearchEngines;
+
+
+/**
+ * url/search input view
+ * handling suggestions
+ */
+public class UrlInputView extends AutoCompleteTextView
+        implements OnEditorActionListener,
+        CompletionListener, OnItemClickListener, TextWatcher {
+
+    static final String TYPED = "browser-type";
+    static final String SUGGESTED = "browser-suggest";
+
+    static final int POST_DELAY = 100;
+
+    static interface StateListener {
+        static final int STATE_NORMAL = 0;
+        static final int STATE_HIGHLIGHTED = 1;
+        static final int STATE_EDITED = 2;
+
+        public void onStateChanged(int state);
+    }
+
+    private UrlInputListener   mListener;
+    private InputMethodManager mInputManager;
+    private SuggestionsAdapter mAdapter;
+    private View mContainer;
+    private boolean mLandscape;
+    private boolean mIncognitoMode;
+    private boolean mNeedsUpdate;
+
+    private int mState;
+    private StateListener mStateListener;
+    private Rect mPopupPadding;
+
+    public UrlInputView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        // SWE_TODO : HARDCODED a random background - clean up
+        /*
+        TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.PopupWindow,
+                R.attr.autoCompleteTextViewStyle, 0);
+
+        Drawable popupbg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
+        a.recycle(); */
+        Drawable popupbg = context.getResources().getDrawable(android.R.drawable.editbox_background);
+        mPopupPadding = new Rect();
+        popupbg.getPadding(mPopupPadding);
+        init(context);
+    }
+
+    public UrlInputView(Context context, AttributeSet attrs) {
+        // SWE_TODO : Needs Fix
+        //this(context, attrs, R.attr.autoCompleteTextViewStyle);
+        this(context, attrs, 0);
+    }
+
+    public UrlInputView(Context context) {
+        this(context, null);
+    }
+
+    private void init(Context ctx) {
+        mInputManager = (InputMethodManager) ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
+        setOnEditorActionListener(this);
+        mAdapter = new SuggestionsAdapter(ctx, this);
+        setAdapter(mAdapter);
+        setSelectAllOnFocus(true);
+        onConfigurationChanged(ctx.getResources().getConfiguration());
+        setThreshold(1);
+        setOnItemClickListener(this);
+        mNeedsUpdate = false;
+        addTextChangedListener(this);
+
+        mState = StateListener.STATE_NORMAL;
+    }
+
+    protected void onFocusChanged(boolean focused, int direction, Rect prevRect) {
+        super.onFocusChanged(focused, direction, prevRect);
+        int state = -1;
+        if (focused) {
+            if (hasSelection()) {
+                state = StateListener.STATE_HIGHLIGHTED;
+            } else {
+                state = StateListener.STATE_EDITED;
+            }
+        } else {
+            // reset the selection state
+            state = StateListener.STATE_NORMAL;
+        }
+        final int s = state;
+        post(new Runnable() {
+            public void run() {
+                changeState(s);
+            }
+        });
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent evt) {
+        boolean hasSelection = hasSelection();
+        boolean res = super.onTouchEvent(evt);
+        if ((MotionEvent.ACTION_DOWN == evt.getActionMasked())
+              && hasSelection) {
+            postDelayed(new Runnable() {
+                public void run() {
+                    changeState(StateListener.STATE_EDITED);
+                }}, POST_DELAY);
+        }
+        return res;
+    }
+
+    /**
+     * check if focus change requires a title bar update
+     */
+    boolean needsUpdate() {
+        return mNeedsUpdate;
+    }
+
+    /**
+     * clear the focus change needs title bar update flag
+     */
+    void clearNeedsUpdate() {
+        mNeedsUpdate = false;
+    }
+
+    void setController(UiController controller) {
+        UrlSelectionActionMode urlSelectionMode
+                = new UrlSelectionActionMode(controller);
+        setCustomSelectionActionModeCallback(urlSelectionMode);
+    }
+
+    void setContainer(View container) {
+        mContainer = container;
+    }
+
+    public void setUrlInputListener(UrlInputListener listener) {
+        mListener = listener;
+    }
+
+    public void setStateListener(StateListener listener) {
+        mStateListener = listener;
+        // update listener
+        changeState(mState);
+    }
+
+    private void changeState(int newState) {
+        mState = newState;
+        if (mStateListener != null) {
+            mStateListener.onStateChanged(mState);
+        }
+    }
+
+    int getState() {
+        return mState;
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration config) {
+        super.onConfigurationChanged(config);
+        mLandscape = (config.orientation &
+                Configuration.ORIENTATION_LANDSCAPE) != 0;
+        mAdapter.setLandscapeMode(mLandscape);
+        if (isPopupShowing() && (getVisibility() == View.VISIBLE)) {
+            setupDropDown();
+            performFiltering(getText(), 0);
+        }
+    }
+
+    @Override
+    public void showDropDown() {
+        setupDropDown();
+        super.showDropDown();
+    }
+
+    @Override
+    public void dismissDropDown() {
+        super.dismissDropDown();
+        mAdapter.clearCache();
+    }
+
+    private void setupDropDown() {
+        int width = mContainer != null ? mContainer.getWidth() : getWidth();
+        width += mPopupPadding.left + mPopupPadding.right;
+        if (width != getDropDownWidth()) {
+            setDropDownWidth(width);
+        }
+        int left = getLeft();
+        left += mPopupPadding.left;
+        if (left != -getDropDownHorizontalOffset()) {
+            setDropDownHorizontalOffset(-left);
+        }
+    }
+
+    @Override
+    public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+        finishInput(getText().toString(), null, TYPED);
+        return true;
+    }
+
+    void forceFilter() {
+        showDropDown();
+    }
+
+    void hideIME() {
+        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
+    }
+
+    void showIME() {
+        //mInputManager.focusIn(this);
+        Object[] params  = {this};
+        Class[] type = new Class[] {View.class};
+        ReflectHelper.invokeMethod(mInputManager, "focusIn", type, params);
+        mInputManager.showSoftInput(this, 0);
+    }
+
+    private void finishInput(String url, String extra, String source) {
+        mNeedsUpdate = true;
+        dismissDropDown();
+        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);
+        if (TextUtils.isEmpty(url)) {
+            mListener.onDismiss();
+        } else {
+            if (mIncognitoMode && isSearch(url)) {
+                // To prevent logging, intercept this request
+                // TODO: This is a quick hack, refactor this
+                SearchEngine searchEngine = BrowserSettings.getInstance()
+                        .getSearchEngine();
+                if (searchEngine == null) return;
+                SearchEngineInfo engineInfo = SearchEngines
+                        .getSearchEngineInfo(getContext(), searchEngine.getName());
+                if (engineInfo == null) return;
+                url = engineInfo.getSearchUriForQuery(url);
+                // mLister.onAction can take it from here without logging
+            }
+            mListener.onAction(url, extra, source);
+        }
+    }
+
+    boolean isSearch(String inUrl) {
+        String url = UrlUtils.fixUrl(inUrl).trim();
+        if (TextUtils.isEmpty(url)) return false;
+
+        if (Patterns.WEB_URL.matcher(url).matches()
+                || UrlUtils.ACCEPTED_URI_SCHEMA.matcher(url).matches()) {
+            return false;
+        }
+        return true;
+    }
+
+    // Completion Listener
+
+    @Override
+    public void onSearch(String search) {
+        mListener.onCopySuggestion(search);
+    }
+
+    @Override
+    public void onSelect(String url, int type, String extra) {
+        finishInput(url, extra, SUGGESTED);
+    }
+
+    @Override
+    public void onItemClick(
+            AdapterView<?> parent, View view, int position, long id) {
+        SuggestItem item = mAdapter.getItem(position);
+        onSelect(SuggestionsAdapter.getSuggestionUrl(item), item.type, item.extra);
+    }
+
+    interface UrlInputListener {
+
+        public void onDismiss();
+
+        public void onAction(String text, String extra, String source);
+
+        public void onCopySuggestion(String text);
+
+    }
+
+    public void setIncognitoMode(boolean incognito) {
+        mIncognitoMode = incognito;
+        mAdapter.setIncognitoMode(mIncognitoMode);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent evt) {
+        if (keyCode == KeyEvent.KEYCODE_ESCAPE && !isInTouchMode()) {
+            finishInput(null, null, null);
+            return true;
+        }
+        return super.onKeyDown(keyCode, evt);
+    }
+
+    public SuggestionsAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    /*
+     * no-op to prevent scrolling of webview when embedded titlebar
+     * gets edited
+     */
+    @Override
+    public boolean requestRectangleOnScreen(Rect rect, boolean immediate) {
+        return false;
+    }
+
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        if (StateListener.STATE_HIGHLIGHTED == mState) {
+            changeState(StateListener.STATE_EDITED);
+        }
+    }
+
+    @Override
+    public void afterTextChanged(Editable s) { }
+
+}
diff --git a/src/com/android/browser/UrlSelectionActionMode.java b/src/com/android/browser/UrlSelectionActionMode.java
new file mode 100644
index 0000000..87446ec
--- /dev/null
+++ b/src/com/android/browser/UrlSelectionActionMode.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2010 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 com.android.browser.R;
+
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
+
+public class UrlSelectionActionMode implements ActionMode.Callback {
+
+    private UiController mUiController;
+
+    public UrlSelectionActionMode(UiController controller) {
+        mUiController = controller;
+    }
+
+    // ActionMode.Callback implementation
+
+    @Override
+    public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+        mode.getMenuInflater().inflate(R.menu.url_selection, menu);
+        return true;
+    }
+
+    @Override
+    public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+        switch (item.getItemId()) {
+            case R.id.share:
+                mUiController.shareCurrentPage();
+                mode.finish();
+                break;
+            default:
+                return false;
+        }
+        return true;
+    }
+
+    @Override
+    public void onDestroyActionMode(ActionMode mode) {
+    }
+
+    @Override
+    public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+        return true;
+    }
+
+}
diff --git a/src/com/android/browser/UrlUtils.java b/src/com/android/browser/UrlUtils.java
new file mode 100755
index 0000000..ff78647
--- /dev/null
+++ b/src/com/android/browser/UrlUtils.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2010 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.net.Uri;
+import android.util.Patterns;
+import android.webkit.URLUtil;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility methods for Url manipulation
+ */
+public class UrlUtils {
+
+    static final Pattern ACCEPTED_URI_SCHEMA = Pattern.compile(
+            "(?i)" + // switch on case insensitive matching
+            "(" +    // begin group for schema
+            "(?:http|https|file):\\/\\/" +
+            "|(?:inline|data|about|javascript):" +
+            ")" +
+            "(.*)" );
+
+    // Google search
+    private final static String QUICKSEARCH_G = "http://www.google.com/m?q=%s";
+    private final static String QUERY_PLACE_HOLDER = "%s";
+
+    // Regular expression to strip http:// and optionally
+    // the trailing slash
+    private static final Pattern STRIP_URL_PATTERN =
+            Pattern.compile("^http://(.*?)/?$");
+
+    private UrlUtils() { /* cannot be instantiated */ }
+
+    /**
+     * Strips the provided url of preceding "http://" and any trailing "/". Does not
+     * strip "https://". If the provided string cannot be stripped, the original string
+     * is returned.
+     *
+     * TODO: Put this in TextUtils to be used by other packages doing something similar.
+     *
+     * @param url a url to strip, like "http://www.google.com/"
+     * @return a stripped url like "www.google.com", or the original string if it could
+     *         not be stripped
+     */
+    public static String stripUrl(String url) {
+        if (url == null) return null;
+        Matcher m = STRIP_URL_PATTERN.matcher(url);
+        if (m.matches()) {
+            return m.group(1);
+        } else {
+            return url;
+        }
+    }
+
+    protected static String smartUrlFilter(Uri inUri) {
+        if (inUri != null) {
+            return smartUrlFilter(inUri.toString());
+        }
+        return null;
+    }
+
+    /**
+     * Attempts to determine whether user input is a URL or search
+     * terms.  Anything with a space is passed to search.
+     *
+     * Converts to lowercase any mistakenly uppercased schema (i.e.,
+     * "Http://" converts to "http://"
+     *
+     * @return Original or modified URL
+     *
+     */
+    public static String smartUrlFilter(String url) {
+        return smartUrlFilter(url, true);
+    }
+
+    /**
+     * Attempts to determine whether user input is a URL or search
+     * terms.  Anything with a space is passed to search if canBeSearch is true.
+     *
+     * Converts to lowercase any mistakenly uppercased schema (i.e.,
+     * "Http://" converts to "http://"
+     *
+     * @param canBeSearch If true, will return a search url if it isn't a valid
+     *                    URL. If false, invalid URLs will return null
+     * @return Original or modified URL
+     *
+     */
+    public static String smartUrlFilter(String url, boolean canBeSearch) {
+        String inUrl = url.trim();
+        boolean hasSpace = inUrl.indexOf(' ') != -1;
+
+        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)) {
+                inUrl = lcScheme + matcher.group(2);
+            }
+            if (hasSpace && Patterns.WEB_URL.matcher(inUrl).matches()) {
+                inUrl = inUrl.replace(" ", "%20");
+            }
+            return inUrl;
+        }
+        if (!hasSpace) {
+            if (Patterns.WEB_URL.matcher(inUrl).matches()) {
+                return URLUtil.guessUrl(inUrl);
+            }
+        }
+        if (canBeSearch) {
+            return URLUtil.composeSearchUrl(inUrl,
+                    QUICKSEARCH_G, QUERY_PLACE_HOLDER);
+        }
+        return null;
+    }
+
+    public static String fixUrl(String inUrl) {
+        // FIXME: Converting the url to lower case
+        // duplicates functionality in smartUrlFilter().
+        // However, changing all current callers of fixUrl to
+        // call smartUrlFilter in addition may have unwanted
+        // consequences, and is deferred for now.
+        int colon = inUrl.indexOf(':');
+        boolean allLower = true;
+        for (int index = 0; index < colon; index++) {
+            char ch = inUrl.charAt(index);
+            if (!Character.isLetter(ch)) {
+                break;
+            }
+            allLower &= Character.isLowerCase(ch);
+            if (index == colon - 1 && !allLower) {
+                inUrl = inUrl.substring(0, colon).toLowerCase()
+                        + inUrl.substring(colon);
+            }
+        }
+        if (inUrl.startsWith("http://") || inUrl.startsWith("https://"))
+            return inUrl;
+        if (inUrl.startsWith("http:") ||
+                inUrl.startsWith("https:")) {
+            if (inUrl.startsWith("http:/") || inUrl.startsWith("https:/")) {
+                inUrl = inUrl.replaceFirst("/", "//");
+            } else inUrl = inUrl.replaceFirst(":", "://");
+        }
+        return inUrl;
+    }
+
+    // Returns the filtered URL. Cannot return null, but can return an empty string
+    /* package */ static String filteredUrl(String inUrl) {
+        if (inUrl == null) {
+            return "";
+        }
+        if (inUrl.startsWith("content:")
+                || inUrl.startsWith("browser:")) {
+            return "";
+        }
+        return inUrl;
+    }
+
+}
diff --git a/src/com/android/browser/WallpaperHandler.java b/src/com/android/browser/WallpaperHandler.java
new file mode 100644
index 0000000..5c539b5
--- /dev/null
+++ b/src/com/android/browser/WallpaperHandler.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (C) 2010 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.ProgressDialog;
+import android.app.WallpaperManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import com.android.browser.R;
+
+/**
+ * Handle setWallpaper requests
+ *
+ */
+public class WallpaperHandler extends Thread
+        implements OnMenuItemClickListener, DialogInterface.OnCancelListener {
+
+    private static final String LOGTAG = "WallpaperHandler";
+    // This should be large enough for BitmapFactory to decode the header so
+    // that we can mark and reset the input stream to avoid duplicate network i/o
+    private static final int BUFFER_SIZE = 128 * 1024;
+
+    private Context mContext;
+    private String  mUrl;
+    private ProgressDialog mWallpaperProgress;
+    private boolean mCanceled = false;
+
+    public WallpaperHandler(Context context, String url) {
+        mContext = context;
+        mUrl = url;
+    }
+
+    @Override
+    public void onCancel(DialogInterface dialog) {
+        mCanceled = true;
+    }
+
+    @Override
+    public boolean onMenuItemClick(MenuItem item) {
+        if (mUrl != null && getState() == State.NEW) {
+            // The user may have tried to set a image with a large file size as
+            // their background so it may take a few moments to perform the
+            // operation.
+            // Display a progress spinner while it is working.
+            mWallpaperProgress = new ProgressDialog(mContext);
+            mWallpaperProgress.setIndeterminate(true);
+            mWallpaperProgress.setMessage(mContext.getResources()
+                    .getText(R.string.progress_dialog_setting_wallpaper));
+            mWallpaperProgress.setCancelable(true);
+            mWallpaperProgress.setOnCancelListener(this);
+            mWallpaperProgress.show();
+            start();
+        }
+        return true;
+    }
+
+    @Override
+    public void run() {
+        WallpaperManager wm = WallpaperManager.getInstance(mContext);
+        Drawable oldWallpaper = wm.getDrawable();
+        InputStream inputstream = null;
+        try {
+            // TODO: This will cause the resource to be downloaded again, when
+            // we should in most cases be able to grab it from the cache. To fix
+            // this we should query WebCore to see if we can access a cached
+            // version and instead open an input stream on that. This pattern
+            // could also be used in the download manager where the same problem
+            // exists.
+            inputstream = openStream();
+            if (inputstream != null) {
+                if (!inputstream.markSupported()) {
+                    inputstream = new BufferedInputStream(inputstream, BUFFER_SIZE);
+                }
+                inputstream.mark(BUFFER_SIZE);
+                BitmapFactory.Options options = new BitmapFactory.Options();
+                options.inJustDecodeBounds = true;
+                // We give decodeStream a wrapped input stream so it doesn't
+                // mess with our mark (currently it sets a mark of 1024)
+                BitmapFactory.decodeStream(
+                        new BufferedInputStream(inputstream), null, options);
+                int maxWidth = wm.getDesiredMinimumWidth();
+                int maxHeight = wm.getDesiredMinimumHeight();
+                // Give maxWidth and maxHeight some leeway
+                maxWidth *= 1.25;
+                maxHeight *= 1.25;
+                int bmWidth = options.outWidth;
+                int bmHeight = options.outHeight;
+
+                int scale = 1;
+                while (bmWidth > maxWidth || bmHeight > maxHeight) {
+                    scale <<= 1;
+                    bmWidth >>= 1;
+                    bmHeight >>= 1;
+                }
+                options.inJustDecodeBounds = false;
+                options.inSampleSize = scale;
+                try {
+                    inputstream.reset();
+                } catch (IOException e) {
+                    // BitmapFactory read more than we could buffer
+                    // Re-open the stream
+                    inputstream.close();
+                    inputstream = openStream();
+                }
+                Bitmap scaledWallpaper = BitmapFactory.decodeStream(inputstream,
+                        null, options);
+                if (scaledWallpaper != null) {
+                    wm.setBitmap(scaledWallpaper);
+                } else {
+                    Log.e(LOGTAG, "Unable to set new wallpaper, " +
+                            "decodeStream returned null.");
+                }
+            }
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Unable to set new wallpaper");
+            // Act as though the user canceled the operation so we try to
+            // restore the old wallpaper.
+            mCanceled = true;
+        } finally {
+            if (inputstream != null) {
+                try {
+                    inputstream.close();
+                } catch (IOException e) {
+                    // Ignore
+                }
+            }
+        }
+
+        if (mCanceled) {
+            // Restore the old wallpaper if the user cancelled whilst we were
+            // setting
+            // the new wallpaper.
+            int width = oldWallpaper.getIntrinsicWidth();
+            int height = oldWallpaper.getIntrinsicHeight();
+            Bitmap bm = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
+            Canvas canvas = new Canvas(bm);
+            oldWallpaper.setBounds(0, 0, width, height);
+            oldWallpaper.draw(canvas);
+            canvas.setBitmap(null);
+            try {
+                wm.setBitmap(bm);
+            } catch (IOException e) {
+                Log.e(LOGTAG, "Unable to restore old wallpaper.");
+            }
+            mCanceled = false;
+        }
+
+        if (mWallpaperProgress.isShowing()) {
+            mWallpaperProgress.dismiss();
+        }
+    }
+
+    /**
+     * Opens the input stream for the URL that the class should
+     * use to set the wallpaper. Abstracts the difference between
+     * standard URLs and data URLs.
+     * @return An open InputStream for the data at the URL
+     * @throws IOException if there is an error opening the URL stream
+     * @throws MalformedURLException if the URL is malformed
+     */
+    private InputStream openStream() throws IOException, MalformedURLException {
+        InputStream inputStream = null;
+        if (DataUri.isDataUri(mUrl)) {
+            DataUri dataUri = new DataUri(mUrl);
+            inputStream = new ByteArrayInputStream(dataUri.getData());
+        } else {
+            URL url = new URL(mUrl);
+            inputStream = url.openStream();
+        }
+        return inputStream;
+    }
+}
diff --git a/src/com/android/browser/WebStorageSizeManager.java b/src/com/android/browser/WebStorageSizeManager.java
new file mode 100644
index 0000000..0a6a514
--- /dev/null
+++ b/src/com/android/browser/WebStorageSizeManager.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2009 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 com.android.browser.R;
+import com.android.browser.preferences.WebsiteSettingsFragment;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.os.StatFs;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.webkit.WebStorage;
+
+import java.io.File;
+
+
+/**
+ * Package level class for managing the disk size consumed by the WebDatabase
+ * and ApplicationCaches APIs (henceforth called Web storage).
+ *
+ * Currently, the situation on the WebKit side is as follows:
+ *  - WebDatabase enforces a quota for each origin.
+ *  - Session/LocalStorage do not enforce any disk limits.
+ *  - ApplicationCaches enforces a maximum size for all origins.
+ *
+ * The WebStorageSizeManager maintains a global limit for the disk space
+ * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will
+ * have a limit for Session/LocalStorage, this class will manage the space used
+ * by those APIs as well.
+ *
+ * The global limit is computed as a function of the size of the partition where
+ * these APIs store their data (they must store it on the same partition for
+ * this to work) and the size of the available space on that partition.
+ * The global limit is not subject to user configuration but we do provide
+ * a debug-only setting.
+ * TODO(andreip): implement the debug setting.
+ *
+ * The size of the disk space used for Web storage is initially divided between
+ * WebDatabase and ApplicationCaches as follows:
+ *
+ * 75% for WebDatabase
+ * 25% for ApplicationCaches
+ *
+ * When an origin's database usage reaches its current quota, WebKit invokes
+ * the following callback function:
+ * - exceededDatabaseQuota(Frame* frame, const String& database_name);
+ * Note that the default quota for a new origin is 0, so we will receive the
+ * 'exceededDatabaseQuota' callback before a new origin gets the chance to
+ * create its first database.
+ *
+ * When the total ApplicationCaches usage reaches its current quota, WebKit
+ * invokes the following callback function:
+ * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
+ *
+ * The WebStorageSizeManager's main job is to respond to the above two callbacks
+ * by inspecting the amount of unused Web storage quota (i.e. global limit -
+ * sum of all other origins' quota) and deciding if a quota increase for the
+ * out-of-space origin is allowed or not.
+ *
+ * The default quota for an origin is its estimated size. If we cannot satisfy
+ * the estimated size, then WebCore will not create the database.
+ * Quota increases are done in steps, where the increase step is
+ * min(QUOTA_INCREASE_STEP, unused_quota).
+ *
+ * When all the Web storage space is used, the WebStorageSizeManager creates
+ * a system notification that will guide the user to the WebSettings UI. There,
+ * the user can free some of the Web storage space by deleting all the data used
+ * by an origin.
+ */
+public class WebStorageSizeManager {
+    // Logging flags.
+    private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED;
+    private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+    private final static String LOGTAG = "browser";
+    // The default quota value for an origin.
+    public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024;  // 3MB
+    // The default value for quota increases.
+    public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024;  // 1MB
+    // Extra padding space for appcache maximum size increases. This is needed
+    // because WebKit sends us an estimate of the amount of space needed
+    // but this estimate may, currently, be slightly less than what is actually
+    // needed. We therefore add some 'padding'.
+    // TODO(andreip): fix this in WebKit.
+    public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB
+    // The system status bar notification id.
+    private final static int OUT_OF_SPACE_ID = 1;
+    // The time of the last out of space notification
+    private static long mLastOutOfSpaceNotificationTime = -1;
+    // Delay between two notification in ms
+    private final static long NOTIFICATION_INTERVAL = 5 * 60 * 1000;
+    // Delay in ms used when resetting the notification time
+    private final static long RESET_NOTIFICATION_INTERVAL = 3 * 1000;
+    // The application context.
+    private final Context mContext;
+    // The global Web storage limit.
+    private final long mGlobalLimit;
+    // The maximum size of the application cache file.
+    private long mAppCacheMaxSize;
+
+    /**
+     * Interface used by the WebStorageSizeManager to obtain information
+     * about the underlying file system. This functionality is separated
+     * into its own interface mainly for testing purposes.
+     */
+    public interface DiskInfo {
+        /**
+         * @return the size of the free space in the file system.
+         */
+        public long getFreeSpaceSizeBytes();
+
+        /**
+         * @return the total size of the file system.
+         */
+        public long getTotalSizeBytes();
+    };
+
+    private DiskInfo mDiskInfo;
+    // For convenience, we provide a DiskInfo implementation that uses StatFs.
+    public static class StatFsDiskInfo implements DiskInfo {
+        private StatFs mFs;
+
+        public StatFsDiskInfo(String path) {
+            mFs = new StatFs(path);
+        }
+
+        public long getFreeSpaceSizeBytes() {
+            return (long)(mFs.getAvailableBlocks()) * mFs.getBlockSize();
+        }
+
+        public long getTotalSizeBytes() {
+            return (long)(mFs.getBlockCount()) * mFs.getBlockSize();
+        }
+    };
+
+    /**
+     * Interface used by the WebStorageSizeManager to obtain information
+     * about the appcache file. This functionality is separated into its own
+     * interface mainly for testing purposes.
+     */
+    public interface AppCacheInfo {
+        /**
+         * @return the current size of the appcache file.
+         */
+        public long getAppCacheSizeBytes();
+    };
+
+    // For convenience, we provide an AppCacheInfo implementation.
+    public static class WebKitAppCacheInfo implements AppCacheInfo {
+        // The name of the application cache file. Keep in sync with
+        // WebCore/loader/appcache/ApplicationCacheStorage.cpp
+        private final static String APPCACHE_FILE = "ApplicationCache.db";
+        private String mAppCachePath;
+
+        public WebKitAppCacheInfo(String path) {
+            mAppCachePath = path;
+        }
+
+        public long getAppCacheSizeBytes() {
+            File file = new File(mAppCachePath
+                    + File.separator
+                    + APPCACHE_FILE);
+            return file.length();
+        }
+    };
+
+    /**
+     * Public ctor
+     * @param ctx is the application context
+     * @param diskInfo is the DiskInfo instance used to query the file system.
+     * @param appCacheInfo is the AppCacheInfo used to query info about the
+     * appcache file.
+     */
+    public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
+            AppCacheInfo appCacheInfo) {
+        mContext = ctx.getApplicationContext();
+        mDiskInfo = diskInfo;
+        mGlobalLimit = getGlobalLimit();
+        // The initial max size of the app cache is either 25% of the global
+        // limit or the current size of the app cache file, whichever is bigger.
+        mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
+                appCacheInfo.getAppCacheSizeBytes());
+    }
+
+    /**
+     * Returns the maximum size of the application cache.
+     */
+    public long getAppCacheMaxSize() {
+        return mAppCacheMaxSize;
+    }
+
+    /**
+     * The origin has exceeded its database quota.
+     * @param url the URL that exceeded the quota
+     * @param databaseIdentifier the identifier of the database on
+     *     which the transaction that caused the quota overflow was run
+     * @param currentQuota the current quota for the origin.
+     * @param estimatedSize the estimated size of a new database, or 0 if
+     *     this has been invoked in response to an existing database
+     *     overflowing its quota.
+     * @param totalUsedQuota is the sum of all origins' quota.
+     * @param quotaUpdater The callback to run when a decision to allow or
+     *     deny quota has been made. Don't forget to call this!
+     */
+    public void onExceededDatabaseQuota(String url,
+        String databaseIdentifier, long currentQuota, long estimatedSize,
+        long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG,
+                  "Received onExceededDatabaseQuota for "
+                  + url
+                  + ":"
+                  + databaseIdentifier
+                  + "(current quota: "
+                  + currentQuota
+                  + ", total used quota: "
+                  + totalUsedQuota
+                  + ")");
+        }
+        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+        if (totalUnusedQuota <= 0) {
+            // There definitely isn't any more space. Fire notifications
+            // if needed and exit.
+            if (totalUsedQuota > 0) {
+                // We only fire the notification if there are some other websites
+                // using some of the quota. This avoids the degenerate case where
+                // the first ever website to use Web storage tries to use more
+                // data than it is actually available. In such a case, showing
+                // the notification would not help at all since there is nothing
+                // the user can do.
+                scheduleOutOfSpaceNotification();
+            }
+            quotaUpdater.updateQuota(currentQuota);
+            if(LOGV_ENABLED) {
+                Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
+            }
+            return;
+        }
+
+        // We have some space inside mGlobalLimit.
+        long newOriginQuota = currentQuota;
+        if (newOriginQuota == 0) {
+            // This is a new origin, give it the size it asked for if possible.
+            // If we cannot satisfy the estimatedSize, we should return 0 as
+            // returning a value less that what the site requested will lead
+            // to webcore not creating the database.
+            if (totalUnusedQuota >= estimatedSize) {
+                newOriginQuota = estimatedSize;
+            } else {
+                if (LOGV_ENABLED) {
+                    Log.v(LOGTAG,
+                            "onExceededDatabaseQuota: Unable to satisfy" +
+                            " estimatedSize for the new database " +
+                            " (estimatedSize: " + estimatedSize +
+                            ", unused quota: " + totalUnusedQuota);
+                }
+                newOriginQuota = 0;
+            }
+        } else {
+            // This is an origin we have seen before. It wants a quota
+            // increase. There are two circumstances: either the origin
+            // is creating a new database or it has overflowed an existing database.
+
+            // Increase the quota. If estimatedSize == 0, then this is a quota overflow
+            // rather than the creation of a new database.
+            long quotaIncrease = estimatedSize == 0 ?
+                    Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota) :
+                    estimatedSize;
+            newOriginQuota += quotaIncrease;
+
+            if (quotaIncrease > totalUnusedQuota) {
+                // We can't fit, so deny quota.
+                newOriginQuota = currentQuota;
+            }
+        }
+
+        quotaUpdater.updateQuota(newOriginQuota);
+
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
+                    + newOriginQuota);
+        }
+    }
+
+    /**
+     * The Application Cache has exceeded its max size.
+     * @param spaceNeeded is the amount of disk space that would be needed
+     * in order for the last appcache operation to succeed.
+     * @param totalUsedQuota is the sum of all origins' quota.
+     * @param quotaUpdater A callback to inform the WebCore thread that a new
+     * app cache size is available. This callback must always be executed at
+     * some point to ensure that the sleeping WebCore thread is woken up.
+     */
+    public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
+            WebStorage.QuotaUpdater quotaUpdater) {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
+                  + spaceNeeded + " bytes.");
+        }
+
+        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+        if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
+            // There definitely isn't any more space. Fire notifications
+            // if needed and exit.
+            if (totalUsedQuota > 0) {
+                // We only fire the notification if there are some other websites
+                // using some of the quota. This avoids the degenerate case where
+                // the first ever website to use Web storage tries to use more
+                // data than it is actually available. In such a case, showing
+                // the notification would not help at all since there is nothing
+                // the user can do.
+                scheduleOutOfSpaceNotification();
+            }
+            quotaUpdater.updateQuota(0);
+            if(LOGV_ENABLED) {
+                Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
+            }
+            return;
+        }
+        // There is enough space to accommodate spaceNeeded bytes.
+        mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
+        quotaUpdater.updateQuota(mAppCacheMaxSize);
+
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
+                    + mAppCacheMaxSize);
+        }
+    }
+
+    // Reset the notification time; we use this iff the user
+    // use clear all; we reset it to some time in the future instead
+    // of just setting it to -1, as the clear all method is asynchronous
+    public static void resetLastOutOfSpaceNotificationTime() {
+        mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
+            NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
+    }
+
+    // Computes the global limit as a function of the size of the data
+    // partition and the amount of free space on that partition.
+    private long getGlobalLimit() {
+        long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
+        long fileSystemSize = mDiskInfo.getTotalSizeBytes();
+        return calculateGlobalLimit(fileSystemSize, freeSpace);
+    }
+
+    /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
+            long freeSpaceBytes) {
+        if (fileSystemSizeBytes <= 0
+                || freeSpaceBytes <= 0
+                || freeSpaceBytes > fileSystemSizeBytes) {
+            return 0;
+        }
+
+        long fileSystemSizeRatio =
+            2 << ((int) Math.floor(Math.log10(
+                    fileSystemSizeBytes / (1024 * 1024))));
+        long maxSizeBytes = (long) Math.min(Math.floor(
+                fileSystemSizeBytes / fileSystemSizeRatio),
+                Math.floor(freeSpaceBytes / 2));
+        // Round maxSizeBytes up to a multiple of 1024KB (but only if
+        // maxSizeBytes > 1MB).
+        long maxSizeStepBytes = 1024 * 1024;
+        if (maxSizeBytes < maxSizeStepBytes) {
+            return 0;
+        }
+        long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
+        return (maxSizeStepBytes
+                * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
+    }
+
+    // Schedules a system notification that takes the user to the WebSettings
+    // activity when clicked.
+    private void scheduleOutOfSpaceNotification() {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
+        }
+        if ((mLastOutOfSpaceNotificationTime == -1) ||
+            (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
+            // setup the notification boilerplate.
+            int icon = android.R.drawable.stat_sys_warning;
+            CharSequence title = mContext.getString(
+                    R.string.webstorage_outofspace_notification_title);
+            CharSequence text = mContext.getString(
+                    R.string.webstorage_outofspace_notification_text);
+            long when = System.currentTimeMillis();
+            Intent intent = new Intent(mContext, BrowserPreferencesPage.class);
+            intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT,
+                    WebsiteSettingsFragment.class.getName());
+            PendingIntent contentIntent =
+                PendingIntent.getActivity(mContext, 0, intent, 0);
+            Notification notification = new Notification(icon, title, when);
+            notification.setLatestEventInfo(mContext, title, text, contentIntent);
+            notification.flags |= Notification.FLAG_AUTO_CANCEL;
+            // Fire away.
+            String ns = Context.NOTIFICATION_SERVICE;
+            NotificationManager mgr =
+                (NotificationManager) mContext.getSystemService(ns);
+            if (mgr != null) {
+                mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
+                mgr.notify(OUT_OF_SPACE_ID, notification);
+            }
+        }
+    }
+}
diff --git a/src/com/android/browser/WebViewController.java b/src/com/android/browser/WebViewController.java
new file mode 100644
index 0000000..6864470
--- /dev/null
+++ b/src/com/android/browser/WebViewController.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2010 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.net.Uri;
+import android.net.http.SslError;
+import android.os.Message;
+import android.view.KeyEvent;
+import android.view.View;
+import org.codeaurora.swe.HttpAuthHandler;
+import org.codeaurora.swe.SslErrorHandler;
+import android.webkit.ValueCallback;
+import android.webkit.WebChromeClient.CustomViewCallback;
+import org.codeaurora.swe.WebChromeClient;
+import org.codeaurora.swe.WebView;
+
+import java.util.List;
+
+/**
+ * WebView aspect of the controller
+ */
+public interface WebViewController {
+
+    Context getContext();
+
+    Activity getActivity();
+
+    TabControl getTabControl();
+
+    WebViewFactory getWebViewFactory();
+
+    void onSetWebView(Tab tab, WebView view);
+
+    void createSubWindow(Tab tab);
+
+    void onPageStarted(Tab tab, WebView view, Bitmap favicon);
+
+    void onPageFinished(Tab tab);
+
+    void onProgressChanged(Tab tab);
+
+    void onReceivedTitle(Tab tab, final String title);
+
+    void onFavicon(Tab tab, WebView view, Bitmap icon);
+
+    boolean shouldOverrideUrlLoading(Tab tab, WebView view, String url);
+
+    boolean shouldOverrideKeyEvent(KeyEvent event);
+
+    boolean onUnhandledKeyEvent(KeyEvent event);
+
+    void doUpdateVisitedHistory(Tab tab, boolean isReload);
+
+    void getVisitedHistory(final ValueCallback<String[]> callback);
+
+    void onReceivedHttpAuthRequest(Tab tab, WebView view, final HttpAuthHandler handler,
+            final String host, final String realm);
+
+    void onDownloadStart(Tab tab, String url, String useragent, String contentDisposition,
+            String mimeType, String referer, long contentLength);
+
+    void showCustomView(Tab tab, View view, int requestedOrientation,
+            CustomViewCallback callback);
+
+    void hideCustomView();
+
+    Bitmap getDefaultVideoPoster();
+
+    View getVideoLoadingProgressView();
+
+    void showSslCertificateOnError(WebView view, SslErrorHandler handler,
+            SslError error);
+
+    void onUserCanceledSsl(Tab tab);
+
+    boolean shouldShowErrorConsole();
+
+    void onUpdatedSecurityState(Tab tab);
+
+    void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture);
+
+    void endActionMode();
+
+    void attachSubWindow(Tab tab);
+
+    void dismissSubWindow(Tab tab);
+
+    Tab openTab(String url, boolean incognito, boolean setActive,
+            boolean useCurrent);
+
+    Tab openTab(String url, Tab parent, boolean setActive,
+            boolean useCurrent);
+
+    boolean switchToTab(Tab tab);
+
+    void closeTab(Tab tab);
+
+    void setupAutoFill(Message message);
+
+    void bookmarkedStatusHasChanged(Tab tab);
+
+    void showAutoLogin(Tab tab);
+
+    void hideAutoLogin(Tab tab);
+
+    boolean shouldCaptureThumbnails();
+}
diff --git a/src/com/android/browser/WebViewFactory.java b/src/com/android/browser/WebViewFactory.java
new file mode 100644
index 0000000..3ebd573
--- /dev/null
+++ b/src/com/android/browser/WebViewFactory.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2010 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 org.codeaurora.swe.WebView;
+
+/**
+ * Factory for WebViews
+ */
+public interface WebViewFactory {
+
+    public WebView createWebView(boolean privateBrowsing);
+
+    public WebView createSubWebView(boolean privateBrowsing);
+
+}
diff --git a/src/com/android/browser/WebViewProperties.java b/src/com/android/browser/WebViewProperties.java
new file mode 100644
index 0000000..c662957
--- /dev/null
+++ b/src/com/android/browser/WebViewProperties.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2011 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;
+
+public interface WebViewProperties {
+    static final String gfxInvertedScreen = "inverted";
+    static final String gfxInvertedScreenContrast = "inverted_contrast";
+    static final String gfxEnableCpuUploadPath = "enable_cpu_upload_path";
+    static final String gfxUseMinimalMemory = "use_minimal_memory";
+}
diff --git a/src/com/android/browser/WebViewTimersControl.java b/src/com/android/browser/WebViewTimersControl.java
new file mode 100644
index 0000000..ac74fa1
--- /dev/null
+++ b/src/com/android/browser/WebViewTimersControl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2011 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.os.Looper;
+import android.util.Log;
+import org.codeaurora.swe.WebView;
+
+/**
+ * Centralised point for controlling WebView timers pausing and resuming.
+ *
+ * All methods on this class should only be called from the UI thread.
+ */
+public class WebViewTimersControl {
+
+    private static final boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
+    private static final String LOGTAG = "WebViewTimersControl";
+
+    private static WebViewTimersControl sInstance;
+
+    private boolean mBrowserActive;
+    private boolean mPrerenderActive;
+
+    /**
+     * Get the static instance. Must be called from UI thread.
+     */
+    public static WebViewTimersControl getInstance() {
+        if (Looper.myLooper() != Looper.getMainLooper()) {
+            throw new IllegalStateException("WebViewTimersControl.get() called on wrong thread");
+        }
+        if (sInstance == null) {
+            sInstance = new WebViewTimersControl();
+        }
+        return sInstance;
+    }
+
+    private WebViewTimersControl() {
+    }
+
+    private void resumeTimers(WebView wv) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "Resuming webview timers, view=" + wv);
+        if (wv != null) {
+            wv.resumeTimers();
+        }
+    }
+
+    private void maybePauseTimers(WebView wv) {
+        if (!mBrowserActive && !mPrerenderActive && wv != null) {
+            if (LOGD_ENABLED) Log.d(LOGTAG, "Pausing webview timers, view=" + wv);
+            wv.pauseTimers();
+        }
+    }
+
+    public void onBrowserActivityResume(WebView wv) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onBrowserActivityResume");
+        mBrowserActive = true;
+        resumeTimers(wv);
+    }
+
+    public void onBrowserActivityPause(WebView wv) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onBrowserActivityPause");
+        mBrowserActive = false;
+        maybePauseTimers(wv);
+    }
+
+    public void onPrerenderStart(WebView wv) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onPrerenderStart");
+        mPrerenderActive = true;
+        resumeTimers(wv);
+    }
+
+    public void onPrerenderDone(WebView wv) {
+        if (LOGD_ENABLED) Log.d(LOGTAG, "onPrerenderDone");
+        mPrerenderActive = false;
+        maybePauseTimers(wv);
+    }
+
+}
diff --git a/src/com/android/browser/XLargeUi.java b/src/com/android/browser/XLargeUi.java
new file mode 100644
index 0000000..8dd31d8
--- /dev/null
+++ b/src/com/android/browser/XLargeUi.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2010 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.ActionBar;
+import android.app.Activity;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.PaintDrawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+import java.util.List;
+
+/**
+ * Ui for xlarge screen sizes
+ */
+public class XLargeUi extends BaseUi {
+
+    private static final String LOGTAG = "XLargeUi";
+
+    private PaintDrawable mFaviconBackground;
+
+    private ActionBar mActionBar;
+    private TabBar mTabBar;
+
+    private NavigationBarTablet mNavBar;
+
+    private Handler mHandler;
+
+    /**
+     * @param browser
+     * @param controller
+     */
+    public XLargeUi(Activity browser, UiController controller) {
+        super(browser, controller);
+        mHandler = new Handler();
+        mNavBar = (NavigationBarTablet) mTitleBar.getNavigationBar();
+        mTabBar = new TabBar(mActivity, mUiController, this);
+        mActionBar = mActivity.getActionBar();
+        setupActionBar();
+        setUseQuickControls(BrowserSettings.getInstance().useQuickControls());
+    }
+
+    private void setupActionBar() {
+        mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+        mActionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
+        mActionBar.setCustomView(mTabBar);
+    }
+
+    public void showComboView(ComboViews startWith, Bundle extras) {
+        super.showComboView(startWith, extras);
+        if (mUseQuickControls) {
+            mActionBar.show();
+        }
+    }
+
+    @Override
+    public void setUseQuickControls(boolean useQuickControls) {
+        super.setUseQuickControls(useQuickControls);
+        checkHideActionBar();
+        if (!useQuickControls) {
+            mActionBar.show();
+        }
+        mTabBar.setUseQuickControls(mUseQuickControls);
+        // We need to update the tabs with this change
+        for (Tab t : mTabControl.getTabs()) {
+            t.updateShouldCaptureThumbnails();
+        }
+    }
+
+    private void checkHideActionBar() {
+        if (mUseQuickControls) {
+            mHandler.post(new Runnable() {
+                public void run() {
+                    mActionBar.hide();
+                }
+            });
+        }
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mNavBar.clearCompletions();
+        checkHideActionBar();
+    }
+
+    @Override
+    public void onDestroy() {
+        hideTitleBar();
+    }
+
+    void stopWebViewScrolling() {
+        BrowserWebView web = (BrowserWebView) mUiController.getCurrentWebView();
+        if (web != null) {
+            web.stopScroll();
+        }
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        MenuItem bm = menu.findItem(R.id.bookmarks_menu_id);
+        if (bm != null) {
+            bm.setVisible(false);
+        }
+        return true;
+    }
+
+
+    // WebView callbacks
+
+    @Override
+    public void addTab(Tab tab) {
+        mTabBar.onNewTab(tab);
+    }
+
+    protected void onAddTabCompleted(Tab tab) {
+        checkHideActionBar();
+    }
+
+    @Override
+    public void setActiveTab(final Tab tab) {
+        mTitleBar.cancelTitleBarAnimation(true);
+        mTitleBar.setSkipTitleBarAnimations(true);
+        super.setActiveTab(tab);
+        BrowserWebView view = (BrowserWebView) tab.getWebView();
+        // TabControl.setCurrentTab has been called before this,
+        // so the tab is guaranteed to have a webview
+        if (view == null) {
+            Log.e(LOGTAG, "active tab with no webview detected");
+            return;
+        }
+        mTabBar.onSetActiveTab(tab);
+        updateLockIconToLatest(tab);
+        mTitleBar.setSkipTitleBarAnimations(false);
+    }
+
+    @Override
+    public void updateTabs(List<Tab> tabs) {
+        mTabBar.updateTabs(tabs);
+        checkHideActionBar();
+    }
+
+    @Override
+    public void removeTab(Tab tab) {
+        mTitleBar.cancelTitleBarAnimation(true);
+        mTitleBar.setSkipTitleBarAnimations(true);
+        super.removeTab(tab);
+        mTabBar.onRemoveTab(tab);
+        mTitleBar.setSkipTitleBarAnimations(false);
+    }
+
+    protected void onRemoveTabCompleted(Tab tab) {
+        checkHideActionBar();
+    }
+
+    int getContentWidth() {
+        if (mContentView != null) {
+            return mContentView.getWidth();
+        }
+        return 0;
+    }
+
+    @Override
+    public void editUrl(boolean clearInput, boolean forceIME) {
+        if (mUseQuickControls) {
+            mTitleBar.setShowProgressOnly(false);
+        }
+        super.editUrl(clearInput, forceIME);
+    }
+
+    // action mode callbacks
+
+    @Override
+    public void onActionModeStarted(ActionMode mode) {
+        if (!mTitleBar.isEditingUrl()) {
+            // hide the title bar when CAB is shown
+            hideTitleBar();
+        }
+    }
+
+    @Override
+    public void onActionModeFinished(boolean inLoad) {
+        checkHideActionBar();
+        if (inLoad) {
+            // the titlebar was removed when the CAB was shown
+            // if the page is loading, show it again
+            if (mUseQuickControls) {
+                mTitleBar.setShowProgressOnly(true);
+            }
+            showTitleBar();
+        }
+    }
+
+    @Override
+    protected void updateNavigationState(Tab tab) {
+        mNavBar.updateNavigationState(tab);
+    }
+
+    @Override
+    public void setUrlTitle(Tab tab) {
+        super.setUrlTitle(tab);
+        mTabBar.onUrlAndTitle(tab, tab.getUrl(), tab.getTitle());
+    }
+
+    // Set the favicon in the title bar.
+    @Override
+    public void setFavicon(Tab tab) {
+        super.setFavicon(tab);
+        mTabBar.onFavicon(tab, tab.getFavicon());
+    }
+
+    @Override
+    public void onHideCustomView() {
+        super.onHideCustomView();
+        checkHideActionBar();
+    }
+
+    @Override
+    public boolean dispatchKey(int code, KeyEvent event) {
+        if (mActiveTab != null) {
+            WebView web = mActiveTab.getWebView();
+            if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                switch (code) {
+                    case KeyEvent.KEYCODE_TAB:
+                    case KeyEvent.KEYCODE_DPAD_UP:
+                    case KeyEvent.KEYCODE_DPAD_LEFT:
+                        if ((web != null) && web.hasFocus() && !mTitleBar.hasFocus()) {
+                            editUrl(false, false);
+                            return true;
+                        }
+                }
+                boolean ctrl = event.hasModifiers(KeyEvent.META_CTRL_ON);
+                if (!ctrl && isTypingKey(event) && !mTitleBar.isEditingUrl()) {
+                    editUrl(true, false);
+                    return mContentView.dispatchKeyEvent(event);
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isTypingKey(KeyEvent evt) {
+        return evt.getUnicodeChar() > 0;
+    }
+
+    TabBar getTabBar() {
+        return mTabBar;
+    }
+
+    @Override
+    public boolean shouldCaptureThumbnails() {
+        return mUseQuickControls;
+    }
+
+    private Drawable getFaviconBackground() {
+        if (mFaviconBackground == null) {
+            mFaviconBackground = new PaintDrawable();
+            Resources res = mActivity.getResources();
+            mFaviconBackground.getPaint().setColor(
+                    res.getColor(R.color.tabFaviconBackground));
+            mFaviconBackground.setCornerRadius(
+                    res.getDimension(R.dimen.tab_favicon_corner_radius));
+        }
+        return mFaviconBackground;
+    }
+
+    @Override
+    public Drawable getFaviconDrawable(Bitmap icon) {
+        Drawable[] array = new Drawable[2];
+        array[0] = getFaviconBackground();
+        if (icon == null) {
+            array[1] = mGenericFavicon;
+        } else {
+            array[1] = new BitmapDrawable(mActivity.getResources(), icon);
+        }
+        LayerDrawable d = new LayerDrawable(array);
+        d.setLayerInset(1, 2, 2, 2, 2);
+        return d;
+    }
+
+}
diff --git a/src/com/android/browser/addbookmark/FolderSpinner.java b/src/com/android/browser/addbookmark/FolderSpinner.java
new file mode 100644
index 0000000..dd85cda
--- /dev/null
+++ b/src/com/android/browser/addbookmark/FolderSpinner.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.addbookmark;
+
+import android.content.Context;
+import android.view.View;
+import android.util.AttributeSet;
+import android.widget.AdapterView;
+import android.widget.Spinner;
+
+/**
+ * Special Spinner class with its own callback for when the selection is set, which
+ * can be ignored by calling setSelectionIgnoringSelectionChange
+ */
+public class FolderSpinner extends Spinner
+        implements AdapterView.OnItemSelectedListener {
+    private OnSetSelectionListener mOnSetSelectionListener;
+    private boolean mFireSetSelection;
+
+    /**
+     * Callback for knowing when the selection has been manually set.  Does not
+     * get called until the selected view has changed.
+     */
+    public interface OnSetSelectionListener {
+        public void onSetSelection(long id);
+    }
+
+    public FolderSpinner(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        super.setOnItemSelectedListener(this);
+    }
+
+    @Override
+    public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
+        // Disallow setting an OnItemSelectedListener, since it is used by us
+        // to fire onSetSelection.
+        throw new RuntimeException("Cannot set an OnItemSelectedListener on a FolderSpinner");
+    }
+
+    public void setOnSetSelectionListener(OnSetSelectionListener l) {
+        mOnSetSelectionListener = l;
+    }
+
+    /**
+     * Call setSelection, without firing the callback
+     * @param position New position to select.
+     */
+    public void setSelectionIgnoringSelectionChange(int position) {
+        super.setSelection(position);
+    }
+
+    @Override
+    public void setSelection(int position) {
+        mFireSetSelection = true;
+        int oldPosition = getSelectedItemPosition();
+        super.setSelection(position);
+        if (mOnSetSelectionListener != null) {
+            if (oldPosition == position) {
+                long id = getAdapter().getItemId(position);
+                // Normally this is not called because the item did not actually
+                // change, but in this case, we still want it to be called.
+                onItemSelected(this, null, position, id);
+            }
+        }
+    }
+
+    @Override
+    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+        if (mFireSetSelection) {
+            mOnSetSelectionListener.onSetSelection(id);
+            mFireSetSelection = false;
+        }
+    }
+
+    @Override
+    public void onNothingSelected(AdapterView<?> parent) {}
+}
+
diff --git a/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java b/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java
new file mode 100644
index 0000000..f86c9c6
--- /dev/null
+++ b/src/com/android/browser/addbookmark/FolderSpinnerAdapter.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2011 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.addbookmark;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.TextView;
+
+/**
+ * SpinnerAdapter used in the AddBookmarkPage to select where to save a
+ * bookmark/folder.
+ */
+public class FolderSpinnerAdapter extends BaseAdapter {
+
+    public static final int HOME_SCREEN = 0;
+    public static final int ROOT_FOLDER = 1;
+    public static final int OTHER_FOLDER = 2;
+    public static final int RECENT_FOLDER = 3;
+
+    private boolean mIncludeHomeScreen;
+    private boolean mIncludesRecentFolder;
+    private long mRecentFolderId;
+    private String mRecentFolderName;
+    private LayoutInflater mInflater;
+    private Context mContext;
+    private String mOtherFolderDisplayText;
+
+    public FolderSpinnerAdapter(Context context, boolean includeHomeScreen) {
+        mIncludeHomeScreen = includeHomeScreen;
+        mContext = context;
+        mInflater = LayoutInflater.from(mContext);
+    }
+
+    public void addRecentFolder(long folderId, String folderName) {
+        mIncludesRecentFolder = true;
+        mRecentFolderId = folderId;
+        mRecentFolderName = folderName;
+    }
+
+    public long recentFolderId() { return mRecentFolderId; }
+
+    private void bindView(int position, View view, boolean isDropDown) {
+        int labelResource;
+        int drawableResource;
+        if (!mIncludeHomeScreen) {
+            position++;
+        }
+        switch (position) {
+            case HOME_SCREEN:
+                labelResource = R.string.add_to_homescreen_menu_option;
+                drawableResource = R.drawable.ic_home_holo_dark;
+                break;
+            case ROOT_FOLDER:
+                labelResource = R.string.add_to_bookmarks_menu_option;
+                drawableResource = R.drawable.ic_bookmarks_holo_dark;
+                break;
+            case RECENT_FOLDER:
+                // Fall through and use the same icon resource
+            case OTHER_FOLDER:
+                labelResource = R.string.add_to_other_folder_menu_option;
+                drawableResource = R.drawable.ic_folder_holo_dark;
+                break;
+            default:
+                labelResource = 0;
+                drawableResource = 0;
+                // assert
+                break;
+        }
+        TextView textView = (TextView) view;
+        if (position == RECENT_FOLDER) {
+            textView.setText(mRecentFolderName);
+        } else if (position == OTHER_FOLDER && !isDropDown
+                && mOtherFolderDisplayText != null) {
+            textView.setText(mOtherFolderDisplayText);
+        } else {
+            textView.setText(labelResource);
+        }
+        textView.setGravity(Gravity.CENTER_VERTICAL);
+        Drawable drawable = mContext.getResources().getDrawable(drawableResource);
+        textView.setCompoundDrawablesWithIntrinsicBounds(drawable, null,
+                null, null);
+    }
+
+    @Override
+    public View getDropDownView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = mInflater.inflate(
+                    android.R.layout.simple_spinner_dropdown_item, parent, false);
+        }
+        bindView(position, convertView, true);
+        return convertView;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = mInflater.inflate(android.R.layout.simple_spinner_item,
+                    parent, false);
+        }
+        bindView(position, convertView, false);
+        return convertView;
+    }
+
+    @Override
+    public int getCount() {
+        int count = 2;
+        if (mIncludeHomeScreen) count++;
+        if (mIncludesRecentFolder) count++;
+        return count;
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return null;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        long id = position;
+        if (!mIncludeHomeScreen) {
+            id++;
+        }
+        return id;
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    public void setOtherFolderDisplayText(String parentTitle) {
+        mOtherFolderDisplayText = parentTitle;
+        notifyDataSetChanged();
+    }
+
+    public void clearRecentFolder() {
+        if (mIncludesRecentFolder) {
+            mIncludesRecentFolder = false;
+            notifyDataSetChanged();
+        }
+    }
+}
diff --git a/src/com/android/browser/homepages/HomeProvider.java b/src/com/android/browser/homepages/HomeProvider.java
new file mode 100644
index 0000000..045cdb8
--- /dev/null
+++ b/src/com/android/browser/homepages/HomeProvider.java
@@ -0,0 +1,124 @@
+
+/*
+ * Copyright (C) 2011 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.homepages;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.util.Log;
+import android.webkit.WebResourceResponse;
+
+import com.android.browser.BrowserSettings;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+public class HomeProvider extends ContentProvider {
+
+    private static final String TAG = "HomeProvider";
+    public static final String AUTHORITY = "com.android.browser.home";
+    public static final String MOST_VISITED = "content://" + AUTHORITY + "/index";
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        return null;
+    }
+
+    @Override
+    public boolean onCreate() {
+        return false;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        return null;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        return 0;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) {
+        try {
+            ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+            final ParcelFileDescriptor write = pipes[1];
+            AssetFileDescriptor afd = new AssetFileDescriptor(write, 0, -1);
+            new RequestHandler(getContext(), uri, afd.createOutputStream()).start();
+            return pipes[0];
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to handle request: " + uri, e);
+            return null;
+        }
+    }
+
+    public static WebResourceResponse shouldInterceptRequest(Context context,
+            String url) {
+        try {
+            boolean useMostVisited = BrowserSettings.getInstance().useMostVisitedHomepage();
+            if (useMostVisited && url.startsWith("content://")) {
+                Uri uri = Uri.parse(url);
+                if (AUTHORITY.equals(uri.getAuthority())) {
+                    InputStream ins = context.getContentResolver()
+                            .openInputStream(uri);
+                    return new WebResourceResponse("text/html", "utf-8", ins);
+                }
+            }
+            boolean listFiles = BrowserSettings.getInstance().isDebugEnabled();
+            if (listFiles && interceptFile(url)) {
+                PipedInputStream ins = new PipedInputStream();
+                PipedOutputStream outs = new PipedOutputStream(ins);
+                new RequestHandler(context, Uri.parse(url), outs).start();
+                return new WebResourceResponse("text/html", "utf-8", ins);
+            }
+        } catch (Exception e) {}
+        return null;
+    }
+
+    private static boolean interceptFile(String url) {
+        if (!url.startsWith("file:///")) {
+            return false;
+        }
+        String fpath = url.substring(7);
+        File f = new File(fpath);
+        if (!f.isDirectory()) {
+            return false;
+        }
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/homepages/RequestHandler.java b/src/com/android/browser/homepages/RequestHandler.java
new file mode 100644
index 0000000..8dbd0ef
--- /dev/null
+++ b/src/com/android/browser/homepages/RequestHandler.java
@@ -0,0 +1,264 @@
+
+/*
+ * Copyright (C) 2011 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.homepages;
+
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import com.android.browser.R;
+import com.android.browser.homepages.Template.ListEntityIterator;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.platformsupport.BrowserContract.History;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.text.DateFormat;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class RequestHandler extends Thread {
+
+    private static final String TAG = "RequestHandler";
+    private static final int INDEX = 1;
+    private static final int RESOURCE = 2;
+    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+    Uri mUri;
+    Context mContext;
+    OutputStream mOutput;
+
+    static {
+        sUriMatcher.addURI(HomeProvider.AUTHORITY, "index", INDEX);
+        sUriMatcher.addURI(HomeProvider.AUTHORITY, "res/*/*", RESOURCE);
+    }
+
+    public RequestHandler(Context context, Uri uri, OutputStream out) {
+        mUri = uri;
+        mContext = context.getApplicationContext();
+        mOutput = out;
+    }
+
+    @Override
+    public void run() {
+        super.run();
+        try {
+            doHandleRequest();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to handle request: " + mUri, e);
+        } finally {
+            cleanup();
+        }
+    }
+
+    void doHandleRequest() throws IOException {
+        if ("file".equals(mUri.getScheme())) {
+            writeFolderIndex();
+            return;
+        }
+        int match = sUriMatcher.match(mUri);
+        switch (match) {
+        case INDEX:
+            writeTemplatedIndex();
+            break;
+        case RESOURCE:
+            writeResource(getUriResourcePath());
+            break;
+        }
+    }
+
+    byte[] htmlEncode(String s) {
+        return TextUtils.htmlEncode(s).getBytes();
+    }
+
+    // We can reuse this for both History and Bookmarks queries because the
+    // columns defined actually belong to the CommonColumn and ImageColumn
+    // interfaces that both History and Bookmarks implement
+    private static final String[] PROJECTION = new String[] {
+        History.URL,
+        History.TITLE,
+        History.THUMBNAIL
+    };
+    private static final String SELECTION = History.URL
+            + " NOT LIKE 'content:%' AND " + History.THUMBNAIL + " IS NOT NULL";
+    void writeTemplatedIndex() throws IOException {
+        Template t = Template.getCachedTemplate(mContext, R.raw.most_visited);
+        Cursor historyResults = mContext.getContentResolver().query(
+                History.CONTENT_URI, PROJECTION, SELECTION,
+                null, History.VISITS + " DESC LIMIT 12");
+        Cursor cursor = historyResults;
+        try {
+            if (cursor.getCount() < 12) {
+                Cursor bookmarkResults = mContext.getContentResolver().query(
+                        Bookmarks.CONTENT_URI, PROJECTION, SELECTION,
+                        null, Bookmarks.DATE_CREATED + " DESC LIMIT 12");
+                cursor = new MergeCursor(new Cursor[] { historyResults, bookmarkResults }) {
+                    @Override
+                    public int getCount() {
+                        return Math.min(12, super.getCount());
+                    }
+                };
+            }
+            t.assignLoop("most_visited", new Template.CursorListEntityWrapper(cursor) {
+                @Override
+                public void writeValue(OutputStream stream, String key) throws IOException {
+                    Cursor cursor = getCursor();
+                    if (key.equals("url")) {
+                        stream.write(htmlEncode(cursor.getString(0)));
+                    } else if (key.equals("title")) {
+                        stream.write(htmlEncode(cursor.getString(1)));
+                    } else if (key.equals("thumbnail")) {
+                        stream.write("data:image/png;base64,".getBytes());
+                        byte[] thumb = cursor.getBlob(2);
+                        stream.write(Base64.encode(thumb, Base64.DEFAULT));
+                    }
+                }
+            });
+            t.write(mOutput);
+        } finally {
+            cursor.close();
+        }
+    }
+
+    private static final Comparator<File> sFileComparator = new Comparator<File>() {
+        @Override
+        public int compare(File lhs, File rhs) {
+            if (lhs.isDirectory() != rhs.isDirectory()) {
+                return lhs.isDirectory() ? -1 : 1;
+            }
+            return lhs.getName().compareTo(rhs.getName());
+        }
+    };
+
+    void writeFolderIndex() throws IOException {
+        File f = new File(mUri.getPath());
+        final File[] files = f.listFiles();
+        Arrays.sort(files, sFileComparator);
+        Template t = Template.getCachedTemplate(mContext, R.raw.folder_view);
+        t.assign("path", mUri.getPath());
+        t.assign("parent_url", f.getParent() != null ? f.getParent() : f.getPath());
+        t.assignLoop("files", new ListEntityIterator() {
+            int index = -1;
+
+            @Override
+            public void writeValue(OutputStream stream, String key) throws IOException {
+                File f = files[index];
+                if ("name".equals(key)) {
+                    stream.write(f.getName().getBytes());
+                }
+                if ("url".equals(key)) {
+                    stream.write(("file://" + f.getAbsolutePath()).getBytes());
+                }
+                if ("type".equals(key)) {
+                    stream.write((f.isDirectory() ? "dir" : "file").getBytes());
+                }
+                if ("size".equals(key)) {
+                    if (f.isFile()) {
+                        stream.write(readableFileSize(f.length()).getBytes());
+                    }
+                }
+                if ("last_modified".equals(key)) {
+                    String date = DateFormat.getDateTimeInstance(
+                            DateFormat.SHORT, DateFormat.SHORT)
+                            .format(f.lastModified());
+                    stream.write(date.getBytes());
+                }
+                if ("alt".equals(key)) {
+                    if (index % 2 == 0) {
+                        stream.write("alt".getBytes());
+                    }
+                }
+            }
+
+            @Override
+            public ListEntityIterator getListIterator(String key) {
+                return null;
+            }
+
+            @Override
+            public void reset() {
+                index = -1;
+            }
+
+            @Override
+            public boolean moveToNext() {
+                return (++index) < files.length;
+            }
+        });
+        t.write(mOutput);
+    }
+
+    static String readableFileSize(long size) {
+        if(size <= 0) return "0";
+        final String[] units = new String[] { "B", "KB", "MB", "GB", "TB" };
+        int digitGroups = (int) (Math.log10(size) / Math.log10(1024));
+        return new DecimalFormat("#,##0.#").format(
+                size / Math.pow(1024, digitGroups)) + " " + units[digitGroups];
+    }
+
+    String getUriResourcePath() {
+        final Pattern pattern = Pattern.compile("/?res/([\\w/]+)");
+        Matcher m = pattern.matcher(mUri.getPath());
+        if (m.matches()) {
+            return m.group(1);
+        } else {
+            return mUri.getPath();
+        }
+    }
+
+    void writeResource(String fileName) throws IOException {
+        Resources res = mContext.getResources();
+        String packageName = R.class.getPackage().getName();
+        int id = res.getIdentifier(fileName, null, packageName);
+        if (id != 0) {
+            InputStream in = res.openRawResource(id);
+            byte[] buf = new byte[4096];
+            int read;
+            while ((read = in.read(buf)) > 0) {
+                mOutput.write(buf, 0, read);
+            }
+        }
+    }
+
+    void writeString(String str) throws IOException {
+        mOutput.write(str.getBytes());
+    }
+
+    void writeString(String str, int offset, int count) throws IOException {
+        mOutput.write(str.getBytes(), offset, count);
+    }
+
+    void cleanup() {
+        try {
+            mOutput.close();
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to close pipe!", e);
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/homepages/Template.java b/src/com/android/browser/homepages/Template.java
new file mode 100644
index 0000000..cf31bbd
--- /dev/null
+++ b/src/com/android/browser/homepages/Template.java
@@ -0,0 +1,281 @@
+
+/*
+ * Copyright (C) 2011 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.homepages;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.util.TypedValue;
+
+import com.android.browser.R;
+
+public class Template {
+
+    private static HashMap<Integer, Template> sCachedTemplates = new HashMap<Integer, Template>();
+
+    public static Template getCachedTemplate(Context context, int id) {
+        synchronized (sCachedTemplates) {
+            Template template = sCachedTemplates.get(id);
+            if (template == null) {
+                template = new Template(context, id);
+                sCachedTemplates.put(id, template);
+            }
+            // Return a copy so that we don't share data
+            return template.copy();
+        }
+    }
+
+    interface Entity {
+        void write(OutputStream stream, EntityData params) throws IOException;
+    }
+
+    interface EntityData {
+        void writeValue(OutputStream stream, String key) throws IOException;
+        ListEntityIterator getListIterator(String key);
+    }
+
+    interface ListEntityIterator extends EntityData {
+        void reset();
+        boolean moveToNext();
+    }
+
+    static class StringEntity implements Entity {
+
+        byte[] mValue;
+
+        public StringEntity(String value) {
+            mValue = value.getBytes();
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            stream.write(mValue);
+        }
+
+    }
+
+    static class SimpleEntity implements Entity {
+
+        String mKey;
+
+        public SimpleEntity(String key) {
+            mKey = key;
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            params.writeValue(stream, mKey);
+        }
+
+    }
+
+    static class ListEntity implements Entity {
+
+        String mKey;
+        Template mSubTemplate;
+
+        public ListEntity(Context context, String key, String subTemplate) {
+            mKey = key;
+            mSubTemplate = new Template(context, subTemplate);
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            ListEntityIterator iter = params.getListIterator(mKey);
+            iter.reset();
+            while (iter.moveToNext()) {
+                mSubTemplate.write(stream, iter);
+            }
+        }
+
+    }
+
+    public abstract static class CursorListEntityWrapper implements ListEntityIterator {
+
+        private Cursor mCursor;
+
+        public CursorListEntityWrapper(Cursor cursor) {
+            mCursor = cursor;
+        }
+
+        @Override
+        public boolean moveToNext() {
+            return mCursor.moveToNext();
+        }
+
+        @Override
+        public void reset() {
+            mCursor.moveToPosition(-1);
+        }
+
+        @Override
+        public ListEntityIterator getListIterator(String key) {
+            return null;
+        }
+
+        public Cursor getCursor() {
+            return mCursor;
+        }
+
+    }
+
+    static class HashMapEntityData implements EntityData {
+
+        HashMap<String, Object> mData;
+
+        public HashMapEntityData(HashMap<String, Object> map) {
+            mData = map;
+        }
+
+        @Override
+        public ListEntityIterator getListIterator(String key) {
+            return (ListEntityIterator) mData.get(key);
+        }
+
+        @Override
+        public void writeValue(OutputStream stream, String key) throws IOException {
+            stream.write((byte[]) mData.get(key));
+        }
+
+    }
+
+    private List<Entity> mTemplate;
+    private HashMap<String, Object> mData = new HashMap<String, Object>();
+    private Template(Context context, int tid) {
+        this(context, readRaw(context, tid));
+    }
+
+    private Template(Context context, String template) {
+        mTemplate = new ArrayList<Entity>();
+        template = replaceConsts(context, template);
+        parseTemplate(context, template);
+    }
+
+    private Template(Template copy) {
+        mTemplate = copy.mTemplate;
+    }
+
+    Template copy() {
+        return new Template(this);
+    }
+
+    void parseTemplate(Context context, String template) {
+        final Pattern pattern = Pattern.compile("<%([=\\{])\\s*(\\w+)\\s*%>");
+        Matcher m = pattern.matcher(template);
+        int start = 0;
+        while (m.find()) {
+            String static_part = template.substring(start, m.start());
+            if (static_part.length() > 0) {
+                mTemplate.add(new StringEntity(static_part));
+            }
+            String type = m.group(1);
+            String name = m.group(2);
+            if (type.equals("=")) {
+                mTemplate.add(new SimpleEntity(name));
+            } else if (type.equals("{")) {
+                Pattern p = Pattern.compile("<%\\}\\s*" + Pattern.quote(name) + "\\s*%>");
+                Matcher end_m = p.matcher(template);
+                if (end_m.find(m.end())) {
+                    start = m.end();
+                    m.region(end_m.end(), template.length());
+                    String subTemplate = template.substring(start, end_m.start());
+                    mTemplate.add(new ListEntity(context, name, subTemplate));
+                    start = end_m.end();
+                    continue;
+                }
+            }
+            start = m.end();
+        }
+        String static_part = template.substring(start, template.length());
+        if (static_part.length() > 0) {
+            mTemplate.add(new StringEntity(static_part));
+        }
+    }
+
+    public void assign(String name, String value) {
+        mData.put(name, value.getBytes());
+    }
+
+    public void assignLoop(String name, ListEntityIterator iter) {
+        mData.put(name, iter);
+    }
+
+    public void write(OutputStream stream) throws IOException {
+        write(stream, new HashMapEntityData(mData));
+    }
+
+    public void write(OutputStream stream, EntityData data) throws IOException {
+        for (Entity ent : mTemplate) {
+            ent.write(stream, data);
+        }
+    }
+
+    private static String replaceConsts(Context context, String template) {
+        final Pattern pattern = Pattern.compile("<%@\\s*(\\w+/\\w+)\\s*%>");
+        final Resources res = context.getResources();
+        final String packageName = R.class.getPackage().getName();
+        Matcher m = pattern.matcher(template);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String name = m.group(1);
+            if (name.startsWith("drawable/")) {
+                m.appendReplacement(sb, "res/" + name);
+            } else {
+                int id = res.getIdentifier(name, null, packageName);
+                if (id != 0) {
+                    TypedValue value = new TypedValue();
+                    res.getValue(id, value, true);
+                    String replacement;
+                    if (value.type == TypedValue.TYPE_DIMENSION) {
+                        float dimen = res.getDimension(id);
+                        int dimeni = (int) dimen;
+                        if (dimeni == dimen)
+                            replacement = Integer.toString(dimeni);
+                        else
+                            replacement = Float.toString(dimen);
+                    } else {
+                        replacement = value.coerceToString().toString();
+                    }
+                    m.appendReplacement(sb, replacement);
+                }
+            }
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    private static String readRaw(Context context, int id) {
+        InputStream ins = context.getResources().openRawResource(id);
+        try {
+            byte[] buf = new byte[ins.available()];
+            ins.read(buf);
+            return new String(buf, "utf-8");
+        } catch (IOException ex) {
+            return "<html><body>Error</body></html>";
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/src/com/android/browser/mynavigation/AddMyNavigationPage.java b/src/com/android/browser/mynavigation/AddMyNavigationPage.java
new file mode 100755
index 0000000..e750aa2
--- /dev/null
+++ b/src/com/android/browser/mynavigation/AddMyNavigationPage.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.ParseException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.util.Log;
+
+import com.android.browser.BrowserUtils;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+import com.android.browser.platformsupport.WebAddress;
+
+import java.io.ByteArrayOutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+public class AddMyNavigationPage extends Activity {
+
+    private static final String LOGTAG = "AddMyNavigationPage";
+    private static final int SAVE_SITE_NAVIGATION = 100;
+
+    private EditText mName;
+    private EditText mAddress;
+    private Button mButtonOK;
+    private Button mButtonCancel;
+    private Bundle mMap;
+    private String mItemUrl;
+    private boolean mIsAdding;
+    private TextView mDialogText;
+    private Handler mHandler;
+
+    private View.OnClickListener mOKListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            if (save()) {
+                AddMyNavigationPage.this.setResult(Activity.RESULT_OK,
+                        (new Intent()).putExtra("need_refresh", true));
+                finish();
+            }
+        }
+    };
+
+    private View.OnClickListener mCancelListener = new View.OnClickListener() {
+        public void onClick(View v) {
+            finish();
+        }
+    };
+
+    protected void onCreate(Bundle icicle) {
+        super.onCreate(icicle);
+        requestWindowFeature(Window.FEATURE_NO_TITLE);
+        setContentView(R.layout.my_navigation_add_page);
+        String name = null;
+        String url = null;
+        mMap = getIntent().getExtras();
+        if (mMap != null) {
+            Bundle b = mMap.getBundle("websites");
+            if (b != null) {
+                mMap = b;
+            }
+            name = mMap.getString("name");
+            url = mMap.getString("url");
+            mIsAdding = mMap.getBoolean("isAdding");
+        }
+
+        // The original url
+        mItemUrl = url;
+        mName = (EditText) findViewById(R.id.title);
+        mAddress = (EditText) findViewById(R.id.address);
+
+        BrowserUtils.maxLengthFilter(AddMyNavigationPage.this, mName,
+                BrowserUtils.FILENAME_MAX_LENGTH);
+        BrowserUtils.maxLengthFilter(AddMyNavigationPage.this, mAddress,
+                BrowserUtils.ADDRESS_MAX_LENGTH);
+
+        if (url.startsWith("ae://") && url.endsWith("add-fav")) {
+            mName.setText("");
+            mAddress.setText("");
+        } else {
+            mName.setText(name);
+            mAddress.setText(url);
+        }
+        mDialogText = (TextView) findViewById(R.id.dialog_title);
+        if (mIsAdding) {
+            mDialogText.setText(R.string.my_navigation_add_label);
+        }
+
+        mButtonOK = (Button) findViewById(R.id.OK);
+        mButtonOK.setOnClickListener(mOKListener);
+        mButtonCancel = (Button) findViewById(R.id.cancel);
+        mButtonCancel.setOnClickListener(mCancelListener);
+
+        if (!getWindow().getDecorView().isInTouchMode()) {
+            mButtonOK.requestFocus();
+        }
+    }
+
+    /**
+     * Runnable to save a website
+     */
+    private class SaveMyNavigationRunnable implements Runnable {
+        private Message mMessage;
+
+        public SaveMyNavigationRunnable(Message msg) {
+            mMessage = msg;
+        }
+
+        public void run() {
+            Bundle bundle = mMessage.getData();
+            String title = bundle.getString("title");
+            String url = bundle.getString("url");
+            String itemUrl = bundle.getString("itemUrl");
+            Boolean toDefaultThumbnail = bundle.getBoolean("toDefaultThumbnail");
+            ContentResolver cr = AddMyNavigationPage.this.getContentResolver();
+            Cursor cursor = null;
+            try {
+                cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                        new String[] {
+                            MyNavigationUtil.ID
+                        }, "url = ?", new String[] {
+                            itemUrl
+                        }, null);
+                if (cursor != null && cursor.moveToFirst()) {
+                    ContentValues values = new ContentValues();
+                    values.put(MyNavigationUtil.TITLE, title);
+                    values.put(MyNavigationUtil.URL, url);
+                    values.put(MyNavigationUtil.WEBSITE, 1 + "");
+                    if (toDefaultThumbnail) {
+                        ByteArrayOutputStream os = new ByteArrayOutputStream();
+                        Bitmap bm = BitmapFactory.decodeResource(
+                                AddMyNavigationPage.this.getResources(),
+                                R.raw.my_navigation_thumbnail_default);
+                        bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                        values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                    }
+                    Uri uri = ContentUris.withAppendedId(MyNavigationUtil.MY_NAVIGATION_URI,
+                            cursor.getLong(0));
+                    cr.update(uri, values, null, null);
+                } else {
+                    Log.e(LOGTAG, "this item does not exist!");
+                }
+            } catch (IllegalStateException e) {
+                Log.e(LOGTAG, "SaveMyNavigationRunnable", e);
+            } finally {
+                if (null != cursor) {
+                    cursor.close();
+                }
+            }
+        }
+    }
+
+    boolean save() {
+        String name = mName.getText().toString().trim();
+        String unfilteredUrl = UrlUtils.fixUrl(mAddress.getText().toString());
+        boolean emptyTitle = name.length() == 0;
+        boolean emptyUrl = unfilteredUrl.trim().length() == 0;
+        Resources r = getResources();
+        if (emptyTitle || emptyUrl) {
+            if (emptyTitle) {
+                mName.setError(r.getText(R.string.website_needs_title));
+            }
+            if (emptyUrl) {
+                mAddress.setError(r.getText(R.string.website_needs_url));
+            }
+            return false;
+        }
+        String url = unfilteredUrl.trim();
+        try {
+            if (!url.toLowerCase().startsWith("javascript:")) {
+                URI uriObj = new URI(url);
+                String scheme = uriObj.getScheme();
+                if (!MyNavigationUtil.urlHasAcceptableScheme(url)) {
+                    if (scheme != null) {
+                        mAddress.setError(r.getText(R.string.my_navigation_cannot_save_url));
+                        return false;
+                    }
+                    WebAddress address;
+                    try {
+                        address = new WebAddress(unfilteredUrl);
+                    } catch (ParseException e) {
+                        throw new URISyntaxException("", "");
+                    }
+                    if (address.getHost().length() == 0) {
+                        throw new URISyntaxException("", "");
+                    }
+                    url = address.toString();
+                } else {
+                    String mark = "://";
+                    int iRet = -1;
+                    if (null != url) {
+                        iRet = url.indexOf(mark);
+                    }
+                    if (iRet > 0 && url.indexOf("/", iRet + mark.length()) < 0) {
+                        url = url + "/";
+                        Log.d(LOGTAG, "URL=" + url);
+                    }
+                }
+            }
+        } catch (URISyntaxException e) {
+            mAddress.setError(r.getText(R.string.bookmark_url_not_valid));
+            return false;
+        }
+
+        // When it is adding, avoid duplicate url that already existing in the
+        // database
+        if (!mItemUrl.equals(url)) {
+            boolean exist = MyNavigationUtil.isMyNavigationUrl(this, url);
+            if (exist) {
+                mAddress.setError(r.getText(R.string.my_navigation_duplicate_url));
+                return false;
+            }
+        }
+        Bundle bundle = new Bundle();
+        bundle.putString("title", name);
+        bundle.putString("url", url);
+        bundle.putString("itemUrl", mItemUrl);
+        if (!mItemUrl.equals(url)) {
+            bundle.putBoolean("toDefaultThumbnail", true);
+        } else {
+            bundle.putBoolean("toDefaultThumbnail", false);
+        }
+        Message msg = Message.obtain(mHandler, SAVE_SITE_NAVIGATION);
+        msg.setData(bundle);
+        Thread t = new Thread(new SaveMyNavigationRunnable(msg));
+        t.start();
+        return true;
+    }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java b/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
new file mode 100755
index 0000000..1de89d4
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationRequestHandler.java
@@ -0,0 +1,174 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.Context;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import com.android.browser.R;
+
+public class MyNavigationRequestHandler extends Thread {
+
+    private static final String LOGTAG = "MyNavigationRequestHandler";
+    private static final int MY_NAVIGATION = 1;
+    private static final int RESOURCE = 2;
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    Uri mUri;
+    Context mContext;
+    OutputStream mOutput;
+
+    static {
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites/res/*/*", RESOURCE);
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites", MY_NAVIGATION);
+    }
+
+    public MyNavigationRequestHandler(Context context, Uri uri, OutputStream out) {
+        mUri = uri;
+        mContext = context.getApplicationContext();
+        mOutput = out;
+    }
+
+    @Override
+    public void run() {
+        super.run();
+        try {
+            doHandleRequest();
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Failed to handle request: " + mUri, e);
+        } finally {
+            cleanup();
+        }
+    }
+
+    void doHandleRequest() throws IOException {
+        int match = URI_MATCHER.match(mUri);
+        switch (match) {
+            case MY_NAVIGATION:
+                writeTemplatedIndex();
+                break;
+            case RESOURCE:
+                writeResource(getUriResourcePath());
+                break;
+            default:
+                break;
+        }
+    }
+
+    private void writeTemplatedIndex() throws IOException {
+        MyNavigationTemplate t = MyNavigationTemplate.getCachedTemplate(mContext,
+                R.raw.my_navigation);
+        Cursor cursor = mContext.getContentResolver().query(
+                Uri.parse("content://com.android.browser.mynavigation/websites"),
+                new String[] {
+                        "url", "title", "thumbnail"
+                },
+                null, null, null);
+
+        t.assignLoop("my_navigation", new MyNavigationTemplate.CursorListEntityWrapper(cursor) {
+            @Override
+            public void writeValue(OutputStream stream, String key) throws IOException {
+                Cursor cursor = getCursor();
+                if (key.equals("url")) {
+                    stream.write(htmlEncode(cursor.getString(0)));
+                } else if (key.equals("title")) {
+                    String title = cursor.getString(1);
+                    if (title == null || title.length() == 0) {
+                        title = mContext.getString(R.string.my_navigation_add);
+                    }
+                    stream.write(htmlEncode(title));
+                } else if (key.equals("thumbnail")) {
+                    stream.write("data:image/png".getBytes());
+                    stream.write(htmlEncode(cursor.getString(0)));
+                    stream.write(";base64,".getBytes());
+                    byte[] thumb = cursor.getBlob(2);
+                    stream.write(Base64.encode(thumb, Base64.DEFAULT));
+                }
+            }
+        });
+        t.write(mOutput);
+        cursor.close();
+    }
+
+    byte[] htmlEncode(String s) {
+        return TextUtils.htmlEncode(s).getBytes();
+    }
+
+    String getUriResourcePath() {
+        final Pattern pattern = Pattern.compile("/?res/([\\w/]+)");
+        Matcher m = pattern.matcher(mUri.getPath());
+        if (m.matches()) {
+            return m.group(1);
+        } else {
+            return mUri.getPath();
+        }
+    }
+
+    void writeResource(String fileName) throws IOException {
+        Resources res = mContext.getResources();
+        String packageName = R.class.getPackage().getName();
+        int id = res.getIdentifier(fileName, null, packageName);
+        if (id != 0) {
+            InputStream in = res.openRawResource(id);
+            byte[] buf = new byte[4096];
+            int read;
+            while ((read = in.read(buf)) > 0) {
+                mOutput.write(buf, 0, read);
+            }
+        }
+    }
+
+    void writeString(String str) throws IOException {
+        mOutput.write(str.getBytes());
+    }
+
+    void writeString(String str, int offset, int count) throws IOException {
+        mOutput.write(str.getBytes(), offset, count);
+    }
+
+    void cleanup() {
+        try {
+            mOutput.close();
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Failed to close pipe!", e);
+        }
+    }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationTemplate.java b/src/com/android/browser/mynavigation/MyNavigationTemplate.java
new file mode 100755
index 0000000..85d1baf
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationTemplate.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.util.TypedValue;
+import android.util.Log;
+
+import com.android.browser.R;
+
+import java.io.InputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class MyNavigationTemplate {
+
+    private static final String LOGTAG = "MyNavigationTemplate";
+    private static HashMap<Integer, MyNavigationTemplate> sCachedTemplates =
+            new HashMap<Integer, MyNavigationTemplate>();
+    private static boolean sCountryChanged = false;
+    private static String sCurrentCountry = "US";
+
+    private List<Entity> mTemplate;
+    private HashMap<String, Object> mData = new HashMap<String, Object>();
+
+    public static MyNavigationTemplate getCachedTemplate(Context context, int id) {
+
+        String changeToCountry = context.getResources().getConfiguration().locale
+                .getDisplayCountry();
+        Log.d(LOGTAG, "MyNavigationTemplate.getCachedTemplate() display country :"
+                + changeToCountry + ", before country :" + sCurrentCountry);
+        if (changeToCountry != null && !changeToCountry.equals(sCurrentCountry)) {
+            sCountryChanged = true;
+            sCurrentCountry = changeToCountry;
+        }
+        synchronized (sCachedTemplates) {
+            MyNavigationTemplate template = sCachedTemplates.get(id);
+            if (template == null || sCountryChanged) {
+                sCountryChanged = false;
+                template = new MyNavigationTemplate(context, id);
+                sCachedTemplates.put(id, template);
+            }
+            return template.copy();
+        }
+    }
+
+    interface Entity {
+        void write(OutputStream stream, EntityData params) throws IOException;
+    }
+
+    interface EntityData {
+        void writeValue(OutputStream stream, String key) throws IOException;
+
+        ListEntityIterator getListIterator(String key);
+    }
+
+    interface ListEntityIterator extends EntityData {
+        void reset();
+
+        boolean moveToNext();
+    }
+
+    static class StringEntity implements Entity {
+        byte[] mValue;
+
+        public StringEntity(String value) {
+            mValue = value.getBytes();
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            stream.write(mValue);
+        }
+    }
+
+    static class SimpleEntity implements Entity {
+        String mKey;
+
+        public SimpleEntity(String key) {
+            mKey = key;
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            params.writeValue(stream, mKey);
+        }
+    }
+
+    static class ListEntity implements Entity {
+        String mKey;
+        MyNavigationTemplate mSubTemplate;
+
+        public ListEntity(Context context, String key, String subTemplate) {
+            mKey = key;
+            mSubTemplate = new MyNavigationTemplate(context, subTemplate);
+        }
+
+        @Override
+        public void write(OutputStream stream, EntityData params) throws IOException {
+            ListEntityIterator iter = params.getListIterator(mKey);
+            if (null == iter) {
+                return;
+            }
+            iter.reset();
+            while (iter.moveToNext()) {
+                mSubTemplate.write(stream, iter);
+            }
+        }
+    }
+
+    public abstract static class CursorListEntityWrapper implements ListEntityIterator {
+        private Cursor mCursor;
+
+        public CursorListEntityWrapper(Cursor cursor) {
+            mCursor = cursor;
+        }
+
+        @Override
+        public boolean moveToNext() {
+            return mCursor.moveToNext();
+        }
+
+        @Override
+        public void reset() {
+            mCursor.moveToPosition(-1);
+        }
+
+        @Override
+        public ListEntityIterator getListIterator(String key) {
+            return null;
+        }
+
+        public Cursor getCursor() {
+            return mCursor;
+        }
+    }
+
+    static class HashMapEntityData implements EntityData {
+        HashMap<String, Object> mData;
+
+        public HashMapEntityData(HashMap<String, Object> map) {
+            mData = map;
+        }
+
+        @Override
+        public ListEntityIterator getListIterator(String key) {
+            return (ListEntityIterator) mData.get(key);
+        }
+
+        @Override
+        public void writeValue(OutputStream stream, String key) throws IOException {
+            stream.write((byte[]) mData.get(key));
+        }
+    }
+
+    private MyNavigationTemplate(Context context, int tid) {
+        this(context, readRaw(context, tid));
+    }
+
+    private MyNavigationTemplate(Context context, String template) {
+        mTemplate = new ArrayList<Entity>();
+        template = replaceConsts(context, template);
+        parseTemplate(context, template);
+    }
+
+    private MyNavigationTemplate(MyNavigationTemplate copy) {
+        mTemplate = copy.mTemplate;
+    }
+
+    MyNavigationTemplate copy() {
+        return new MyNavigationTemplate(this);
+    }
+
+    void parseTemplate(Context context, String template) {
+        final Pattern pattern = Pattern.compile("<%([=\\{])\\s*(\\w+)\\s*%>");
+        Matcher m = pattern.matcher(template);
+        int start = 0;
+        while (m.find()) {
+            String staticPart = template.substring(start, m.start());
+            if (staticPart.length() > 0) {
+                mTemplate.add(new StringEntity(staticPart));
+            }
+            String type = m.group(1);
+            String name = m.group(2);
+            if (type.equals("=")) {
+                mTemplate.add(new SimpleEntity(name));
+            } else if (type.equals("{")) {
+                Pattern p = Pattern.compile("<%\\}\\s*" + Pattern.quote(name) + "\\s*%>");
+                Matcher end = p.matcher(template);
+                if (end.find(m.end())) {
+                    start = m.end();
+                    m.region(end.end(), template.length());
+                    String subTemplate = template.substring(start, end.start());
+                    mTemplate.add(new ListEntity(context, name, subTemplate));
+                    start = end.end();
+                    continue;
+                }
+            }
+            start = m.end();
+        }
+        String staticPart = template.substring(start, template.length());
+        if (staticPart.length() > 0) {
+            mTemplate.add(new StringEntity(staticPart));
+        }
+    }
+
+    public void assign(String name, String value) {
+        mData.put(name, value.getBytes());
+    }
+
+    public void assignLoop(String name, ListEntityIterator iter) {
+        mData.put(name, iter);
+    }
+
+    public void write(OutputStream stream) throws IOException {
+        write(stream, new HashMapEntityData(mData));
+    }
+
+    public void write(OutputStream stream, EntityData data) throws IOException {
+        for (Entity ent : mTemplate) {
+            ent.write(stream, data);
+        }
+    }
+
+    private static String replaceConsts(Context context, String template) {
+        final Pattern pattern = Pattern.compile("<%@\\s*(\\w+/\\w+)\\s*%>");
+        final Resources res = context.getResources();
+        final String packageName = R.class.getPackage().getName();
+        Matcher m = pattern.matcher(template);
+        StringBuffer sb = new StringBuffer();
+        while (m.find()) {
+            String name = m.group(1);
+            if (name.startsWith("drawable/")) {
+                m.appendReplacement(sb, "res/" + name);
+            } else {
+                int id = res.getIdentifier(name, null, packageName);
+                if (id != 0) {
+                    TypedValue value = new TypedValue();
+                    res.getValue(id, value, true);
+                    String replacement;
+                    if (value.type == TypedValue.TYPE_DIMENSION) {
+                        float dimen = res.getDimension(id);
+                        int dimeni = (int) dimen;
+                        if (dimeni == dimen) {
+                            replacement = Integer.toString(dimeni);
+                        } else {
+                            replacement = Float.toString(dimen);
+                        }
+                    } else {
+                        replacement = value.coerceToString().toString();
+                    }
+                    m.appendReplacement(sb, replacement);
+                }
+            }
+        }
+        m.appendTail(sb);
+        return sb.toString();
+    }
+
+    private static String readRaw(Context context, int id) {
+        InputStream ins = context.getResources().openRawResource(id);
+        try {
+            byte[] buf = new byte[ins.available()];
+            ins.read(buf);
+            return new String(buf, "utf-8");
+        } catch (IOException ex) {
+            return "<html><body>Error</body></html>";
+        }
+    }
+}
diff --git a/src/com/android/browser/mynavigation/MyNavigationUtil.java b/src/com/android/browser/mynavigation/MyNavigationUtil.java
new file mode 100755
index 0000000..3b1836d
--- /dev/null
+++ b/src/com/android/browser/mynavigation/MyNavigationUtil.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.mynavigation;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.ContentResolver;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+public class MyNavigationUtil {
+
+    public static final String ID = "_id";
+    public static final String URL = "url";
+    public static final String TITLE = "title";
+    public static final String DATE_CREATED = "created";
+    public static final String WEBSITE = "website";
+    public static final String FAVICON = "favicon";
+    public static final String THUMBNAIL = "thumbnail";
+    public static final int WEBSITE_NUMBER = 12;
+
+    public static final String AUTHORITY = "com.android.browser.mynavigation";
+    public static final String MY_NAVIGATION = "content://" + AUTHORITY + "/" + "websites";
+    public static final Uri MY_NAVIGATION_URI = Uri
+            .parse("content://com.android.browser.mynavigation/websites");
+    public static final String DEFAULT_THUMB = "default_thumb";
+    public static final String LOGTAG = "MyNavigationUtil";
+
+    public static boolean isDefaultMyNavigation(String url) {
+        if (url != null && url.startsWith("ae://") && url.endsWith("add-fav")) {
+            Log.d(LOGTAG, "isDefaultMyNavigation will return true.");
+            return true;
+        }
+        return false;
+    }
+
+    public static String getMyNavigationUrl(String srcUrl) {
+        String srcPrefix = "data:image/png";
+        String srcSuffix = ";base64,";
+        if (srcUrl != null && srcUrl.startsWith(srcPrefix)) {
+            int indexPrefix = srcPrefix.length();
+            int indexSuffix = srcUrl.indexOf(srcSuffix);
+            return srcUrl.substring(indexPrefix, indexSuffix);
+        }
+        return "";
+    }
+
+    public static boolean isMyNavigationUrl(Context context, String itemUrl) {
+        ContentResolver cr = context.getContentResolver();
+        Cursor cursor = null;
+        try {
+            cursor = cr.query(MyNavigationUtil.MY_NAVIGATION_URI,
+                    new String[] {
+                        MyNavigationUtil.TITLE
+                    }, "url = ?", new String[] {
+                        itemUrl
+                    }, null);
+            if (null != cursor && cursor.moveToFirst()) {
+                Log.d(LOGTAG, "isMyNavigationUrl will return true.");
+                return true;
+            }
+        } catch (IllegalStateException e) {
+            Log.e(LOGTAG, "isMyNavigationUrl", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+        return false;
+    }
+
+    private static final String ACCEPTABLE_WEBSITE_SCHEMES[] = {
+            "http:",
+            "https:",
+            "about:",
+            "data:",
+            "javascript:",
+            "file:",
+            "content:"
+    };
+
+    public static boolean urlHasAcceptableScheme(String url) {
+        if (url == null) {
+            return false;
+        }
+
+        for (int i = 0; i < ACCEPTABLE_WEBSITE_SCHEMES.length; i++) {
+            if (url.startsWith(ACCEPTABLE_WEBSITE_SCHEMES[i])) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/browser/platformsupport/BookmarkColumns.java b/src/com/android/browser/platformsupport/BookmarkColumns.java
new file mode 100644
index 0000000..c1d9db8
--- /dev/null
+++ b/src/com/android/browser/platformsupport/BookmarkColumns.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * Copyright (C) 2006 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.platformsupport;
+
+public class BookmarkColumns {
+    /**
+     * The URL of the bookmark or history item.
+     * <p>Type: TEXT (URL)</p>
+     */
+    public static final String URL = "url";
+
+    /**
+     * The number of time the item has been visited.
+     * <p>Type: NUMBER</p>
+     */
+    public static final String VISITS = "visits";
+
+    /**
+     * The date the item was last visited, in milliseconds since the epoch.
+     * <p>Type: NUMBER (date in milliseconds since January 1, 1970)</p>
+     */
+    public static final String DATE = "date";
+
+    /**
+     * Flag indicating that an item is a bookmark. A value of 1 indicates a bookmark, a value
+     * of 0 indicates a history item.
+     * <p>Type: INTEGER (boolean)</p>
+     */
+    public static final String BOOKMARK = "bookmark";
+
+    /**
+     * The user visible title of the bookmark or history item.
+     * <p>Type: TEXT</p>
+     */
+    public static final String TITLE = "title";
+
+    /**
+     * The date the item created, in milliseconds since the epoch.
+     * <p>Type: NUMBER (date in milliseconds since January 1, 1970)</p>
+     */
+    public static final String CREATED = "created";
+
+    /**
+     * The favicon of the bookmark. Must decode via {@link BitmapFactory#decodeByteArray}.
+     * <p>Type: BLOB (image)</p>
+     */
+    public static final String FAVICON = "favicon";
+
+    /**
+     * @hide
+     */
+    public static final String THUMBNAIL = "thumbnail";
+
+    /**
+     * @hide
+     */
+    public static final String TOUCH_ICON = "touch_icon";
+
+    /**
+     * @hide
+     */
+    public static final String USER_ENTERED = "user_entered";
+}
diff --git a/src/com/android/browser/platformsupport/BrowserContract.java b/src/com/android/browser/platformsupport/BrowserContract.java
new file mode 100644
index 0000000..755e6a3
--- /dev/null
+++ b/src/com/android/browser/platformsupport/BrowserContract.java
@@ -0,0 +1,744 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * 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.platformsupport;
+
+import android.accounts.Account;
+import android.content.ContentProviderClient;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.util.Pair;
+import android.provider.SyncStateContract;
+/**
+ * <p>
+ * The contract between the browser provider and applications. Contains the definition
+ * for the supported URIS and columns.
+ * </p>
+ * <h3>Overview</h3>
+ * <p>
+ * BrowserContract defines an database of browser-related information which are bookmarks,
+ * history, images and the mapping between the image and URL.
+ * </p>
+ * @hide
+ */
+public class BrowserContract {
+    /** The authority for the browser provider */
+    public static final String AUTHORITY = "com.android.browser";
+
+    /** A content:// style uri to the authority for the browser provider */
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    /**
+     * An optional insert, update or delete URI parameter that allows the caller
+     * to specify that it is a sync adapter. The default value is false. If true
+     * the dirty flag is not automatically set and the "syncToNetwork" parameter
+     * is set to false when calling
+     * {@link ContentResolver#notifyChange(android.net.Uri, android.database.ContentObserver, boolean)}.
+     * @hide
+     */
+    public static final String CALLER_IS_SYNCADAPTER = "caller_is_syncadapter";
+
+    /**
+     * A parameter for use when querying any table that allows specifying a limit on the number
+     * of rows returned.
+     * @hide
+     */
+    public static final String PARAM_LIMIT = "limit";
+
+    /**
+     * Generic columns for use by sync adapters. The specific functions of
+     * these columns are private to the sync adapter. Other clients of the API
+     * should not attempt to either read or write these columns.
+     *
+     * @hide
+     */
+    interface BaseSyncColumns {
+        /** Generic column for use by sync adapters. */
+        public static final String SYNC1 = "sync1";
+        /** Generic column for use by sync adapters. */
+        public static final String SYNC2 = "sync2";
+        /** Generic column for use by sync adapters. */
+        public static final String SYNC3 = "sync3";
+        /** Generic column for use by sync adapters. */
+        public static final String SYNC4 = "sync4";
+        /** Generic column for use by sync adapters. */
+        public static final String SYNC5 = "sync5";
+    }
+
+    /**
+     * Convenience definitions for use in implementing chrome bookmarks sync in the Bookmarks table.
+     * @hide
+     */
+    public static final class ChromeSyncColumns {
+        private ChromeSyncColumns() {}
+
+        /** The server unique ID for an item */
+        public static final String SERVER_UNIQUE = BaseSyncColumns.SYNC3;
+
+        public static final String FOLDER_NAME_ROOT = "google_chrome";
+        public static final String FOLDER_NAME_BOOKMARKS = "google_chrome_bookmarks";
+        public static final String FOLDER_NAME_BOOKMARKS_BAR = "bookmark_bar";
+        public static final String FOLDER_NAME_OTHER_BOOKMARKS = "other_bookmarks";
+
+        /** The client unique ID for an item */
+        public static final String CLIENT_UNIQUE = BaseSyncColumns.SYNC4;
+    }
+
+    /**
+     * Columns that appear when each row of a table belongs to a specific
+     * account, including sync information that an account may need.
+     * @hide
+     */
+    interface SyncColumns extends BaseSyncColumns {
+        /**
+         * The name of the account instance to which this row belongs, which when paired with
+         * {@link #ACCOUNT_TYPE} identifies a specific account.
+         * <P>Type: TEXT</P>
+         */
+        public static final String ACCOUNT_NAME = "account_name";
+
+        /**
+         * The type of account to which this row belongs, which when paired with
+         * {@link #ACCOUNT_NAME} identifies a specific account.
+         * <P>Type: TEXT</P>
+         */
+        public static final String ACCOUNT_TYPE = "account_type";
+
+        /**
+         * String that uniquely identifies this row to its source account.
+         * <P>Type: TEXT</P>
+         */
+        public static final String SOURCE_ID = "sourceid";
+
+        /**
+         * Version number that is updated whenever this row or its related data
+         * changes.
+         * <P>Type: INTEGER</P>
+         */
+        public static final String VERSION = "version";
+
+        /**
+         * Flag indicating that {@link #VERSION} has changed, and this row needs
+         * to be synchronized by its owning account.
+         * <P>Type: INTEGER (boolean)</P>
+         */
+        public static final String DIRTY = "dirty";
+
+        /**
+         * The time that this row was last modified by a client (msecs since the epoch).
+         * <P>Type: INTEGER</P>
+         */
+        public static final String DATE_MODIFIED = "modified";
+    }
+
+    interface CommonColumns {
+        /**
+         * The unique ID for a row.
+         * <P>Type: INTEGER (long)</P>
+         */
+        public static final String _ID = "_id";
+
+        /**
+         * This column is valid when the row is a URL. The history table's URL
+         * can not be updated.
+         * <P>Type: TEXT (URL)</P>
+         */
+        public static final String URL = "url";
+
+        /**
+         * The user visible title.
+         * <P>Type: TEXT</P>
+         */
+        public static final String TITLE = "title";
+
+        /**
+         * The time that this row was created on its originating client (msecs
+         * since the epoch).
+         * <P>Type: INTEGER</P>
+         * @hide
+         */
+        public static final String DATE_CREATED = "created";
+    }
+
+    /**
+     * @hide
+     */
+    interface ImageColumns {
+        /**
+         * The favicon of the bookmark, may be NULL.
+         * Must decode via {@link BitmapFactory#decodeByteArray}.
+         * <p>Type: BLOB (image)</p>
+         */
+        public static final String FAVICON = "favicon";
+
+        /**
+         * A thumbnail of the page,may be NULL.
+         * Must decode via {@link BitmapFactory#decodeByteArray}.
+         * <p>Type: BLOB (image)</p>
+         */
+        public static final String THUMBNAIL = "thumbnail";
+
+        /**
+         * The touch icon for the web page, may be NULL.
+         * Must decode via {@link BitmapFactory#decodeByteArray}.
+         * <p>Type: BLOB (image)</p>
+         */
+        public static final String TOUCH_ICON = "touch_icon";
+    }
+
+    interface HistoryColumns {
+        /**
+         * The date the item was last visited, in milliseconds since the epoch.
+         * <p>Type: INTEGER (date in milliseconds since January 1, 1970)</p>
+         */
+        public static final String DATE_LAST_VISITED = "date";
+
+        /**
+         * The number of times the item has been visited.
+         * <p>Type: INTEGER</p>
+         */
+        public static final String VISITS = "visits";
+
+        /**
+         * @hide
+         */
+        public static final String USER_ENTERED = "user_entered";
+    }
+
+    interface ImageMappingColumns {
+        /**
+         * The ID of the image in Images. One image can map onto the multiple URLs.
+         * <P>Type: INTEGER (long)</P>
+         */
+        public static final String IMAGE_ID = "image_id";
+
+        /**
+         * The URL. The URL can map onto the different type of images.
+         * <P>Type: TEXT (URL)</P>
+         */
+        public static final String URL = "url";
+    }
+
+    /**
+     * The bookmarks table, which holds the user's browser bookmarks.
+     */
+    public static final class Bookmarks implements CommonColumns, ImageColumns, SyncColumns {
+        /**
+         * This utility class cannot be instantiated.
+         */
+        private Bookmarks() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "bookmarks");
+
+        /**
+         * Used in {@link Bookmarks#TYPE} column and indicats the row is a bookmark.
+         */
+        public static final int BOOKMARK_TYPE_BOOKMARK = 1;
+
+        /**
+         * Used in {@link Bookmarks#TYPE} column and indicats the row is a folder.
+         */
+        public static final int BOOKMARK_TYPE_FOLDER = 2;
+
+        /**
+         * Used in {@link Bookmarks#TYPE} column and indicats the row is the bookmark bar folder.
+         */
+        public static final int BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER = 3;
+
+        /**
+         * Used in {@link Bookmarks#TYPE} column and indicats the row is other folder and
+         */
+        public static final int BOOKMARK_TYPE_OTHER_FOLDER = 4;
+
+        /**
+         * Used in {@link Bookmarks#TYPE} column and indicats the row is other folder, .
+         */
+        public static final int BOOKMARK_TYPE_MOBILE_FOLDER = 5;
+
+        /**
+         * The type of the item.
+         * <P>Type: INTEGER</P>
+         * <p>Allowed values are:</p>
+         * <p>
+         * <ul>
+         * <li>{@link #BOOKMARK_TYPE_BOOKMARK}</li>
+         * <li>{@link #BOOKMARK_TYPE_FOLDER}</li>
+         * <li>{@link #BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER}</li>
+         * <li>{@link #BOOKMARK_TYPE_OTHER_FOLDER}</li>
+         * <li>{@link #BOOKMARK_TYPE_MOBILE_FOLDER}</li>
+         * </ul>
+         * </p>
+         * <p> The TYPE_BOOKMARK_BAR_FOLDER, TYPE_OTHER_FOLDER and TYPE_MOBILE_FOLDER
+         * can not be updated or deleted.</p>
+         */
+        public static final String TYPE = "type";
+
+        /**
+         * The content:// style URI for the default folder
+         * @hide
+         */
+        public static final Uri CONTENT_URI_DEFAULT_FOLDER =
+                Uri.withAppendedPath(CONTENT_URI, "folder");
+
+        /**
+         * Query parameter used to specify an account name
+         * @hide
+         */
+        public static final String PARAM_ACCOUNT_NAME = "acct_name";
+
+        /**
+         * Query parameter used to specify an account type
+         * @hide
+         */
+        public static final String PARAM_ACCOUNT_TYPE = "acct_type";
+
+        /**
+         * Builds a URI that points to a specific folder.
+         * @param folderId the ID of the folder to point to
+         * @hide
+         */
+        public static final Uri buildFolderUri(long folderId) {
+            return ContentUris.withAppendedId(CONTENT_URI_DEFAULT_FOLDER, folderId);
+        }
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of bookmarks.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/bookmark";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} of a single bookmark.
+         */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/bookmark";
+
+        /**
+         * Query parameter to use if you want to see deleted bookmarks that are still
+         * around on the device and haven't been synced yet.
+         * @see #IS_DELETED
+         * @hide
+         */
+        public static final String QUERY_PARAMETER_SHOW_DELETED = "show_deleted";
+
+        /**
+         * Flag indicating if an item is a folder or bookmark. Non-zero values indicate
+         * a folder and zero indicates a bookmark.
+         * <P>Type: INTEGER (boolean)</P>
+         * @hide
+         */
+        public static final String IS_FOLDER = "folder";
+
+        /**
+         * The ID of the parent folder. ID 0 is the root folder.
+         * <P>Type: INTEGER (reference to item in the same table)</P>
+         */
+        public static final String PARENT = "parent";
+
+        /**
+         * The source ID for an item's parent. Read-only.
+         * @see #PARENT
+         * @hide
+         */
+        public static final String PARENT_SOURCE_ID = "parent_source";
+
+        /**
+         * The position of the bookmark in relation to it's siblings that share the same
+         * {@link #PARENT}. May be negative.
+         * <P>Type: INTEGER</P>
+         * @hide
+         */
+        public static final String POSITION = "position";
+
+        /**
+         * The item that the bookmark should be inserted after.
+         * May be negative.
+         * <P>Type: INTEGER</P>
+         * @hide
+         */
+        public static final String INSERT_AFTER = "insert_after";
+
+        /**
+         * The source ID for the item that the bookmark should be inserted after. Read-only.
+         * May be negative.
+         * <P>Type: INTEGER</P>
+         * @see #INSERT_AFTER
+         * @hide
+         */
+        public static final String INSERT_AFTER_SOURCE_ID = "insert_after_source";
+
+        /**
+         * A flag to indicate if an item has been deleted. Queries will not return deleted
+         * entries unless you add the {@link #QUERY_PARAMETER_SHOW_DELETED} query paramter
+         * to the URI when performing your query.
+         * <p>Type: INTEGER (non-zero if the item has been deleted, zero if it hasn't)
+         * @see #QUERY_PARAMETER_SHOW_DELETED
+         * @hide
+         */
+        public static final String IS_DELETED = "deleted";
+    }
+
+    /**
+     * Read-only table that lists all the accounts that are used to provide bookmarks.
+     * @hide
+     */
+    public static final class Accounts {
+        /**
+         * Directory under {@link Bookmarks#CONTENT_URI}
+         */
+        public static final Uri CONTENT_URI =
+                AUTHORITY_URI.buildUpon().appendPath("accounts").build();
+
+        /**
+         * The name of the account instance to which this row belongs, which when paired with
+         * {@link #ACCOUNT_TYPE} identifies a specific account.
+         * <P>Type: TEXT</P>
+         */
+        public static final String ACCOUNT_NAME = "account_name";
+
+        /**
+         * The type of account to which this row belongs, which when paired with
+         * {@link #ACCOUNT_NAME} identifies a specific account.
+         * <P>Type: TEXT</P>
+         */
+        public static final String ACCOUNT_TYPE = "account_type";
+
+        /**
+         * The ID of the account's root folder. This will be the ID of the folder
+         * returned when querying {@link Bookmarks#CONTENT_URI_DEFAULT_FOLDER}.
+         * <P>Type: INTEGER</P>
+         */
+        public static final String ROOT_ID = "root_id";
+    }
+
+    /**
+     * The history table, which holds the browsing history.
+     */
+    public static final class History implements CommonColumns, HistoryColumns, ImageColumns {
+        /**
+         * This utility class cannot be instantiated.
+         */
+        private History() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "history");
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of browser history items.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/browser-history";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} of a single browser history item.
+         */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/browser-history";
+    }
+
+    /**
+     * The search history table.
+     * @hide
+     */
+    public static final class Searches {
+        private Searches() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "searches");
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of browser search items.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/searches";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} of a single browser search item.
+         */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/searches";
+
+        /**
+         * The unique ID for a row.
+         * <P>Type: INTEGER (long)</P>
+         */
+        public static final String _ID = "_id";
+
+        /**
+         * The user entered search term.
+         */
+        public static final String SEARCH = "search";
+
+        /**
+         * The date the search was performed, in milliseconds since the epoch.
+         * <p>Type: NUMBER (date in milliseconds since January 1, 1970)</p>
+         */
+        public static final String DATE = "date";
+    }
+
+    /**
+     * A table provided for sync adapters to use for storing private sync state data.
+     *
+     * @see SyncStateContract
+     * @hide
+     */
+    public static final class SyncState implements SyncStateContract.Columns {
+        /**
+         * This utility class cannot be instantiated
+         */
+        private SyncState() {}
+
+        public static final String CONTENT_DIRECTORY =
+                SyncStateContract.Constants.CONTENT_DIRECTORY;
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI =
+                Uri.withAppendedPath(AUTHORITY_URI, CONTENT_DIRECTORY);
+
+        /**
+         * @see android.provider.SyncStateContract.Helpers#get
+         */
+        public static byte[] get(ContentProviderClient provider, Account account)
+                throws RemoteException {
+            return SyncStateContract.Helpers.get(provider, CONTENT_URI, account);
+        }
+
+        /**
+         * @see android.provider.SyncStateContract.Helpers#get
+         */
+        public static Pair<Uri, byte[]> getWithUri(ContentProviderClient provider, Account account)
+                throws RemoteException {
+            return SyncStateContract.Helpers.getWithUri(provider, CONTENT_URI, account);
+        }
+
+        /**
+         * @see android.provider.SyncStateContract.Helpers#set
+         */
+        public static void set(ContentProviderClient provider, Account account, byte[] data)
+                throws RemoteException {
+            SyncStateContract.Helpers.set(provider, CONTENT_URI, account, data);
+        }
+
+        /**
+         * @see android.provider.SyncStateContract.Helpers#newSetOperation
+         */
+        public static ContentProviderOperation newSetOperation(Account account, byte[] data) {
+            return SyncStateContract.Helpers.newSetOperation(CONTENT_URI, account, data);
+        }
+    }
+
+    /**
+     * <p>
+     * Stores images for URLs.
+     * </p>
+     * <p>
+     * The rows in this table can not be updated since there might have multiple URLs mapping onto
+     * the same image. If you want to update a URL's image, you need to add the new image in this
+     * table, then update the mapping onto the added image.
+     * </p>
+     * <p>
+     * Every image should be at least associated with one URL, otherwise it will be removed after a
+     * while.
+     * </p>
+     */
+    public static final class Images implements ImageColumns {
+        /**
+         * This utility class cannot be instantiated
+         */
+        private Images() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "images");
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of images.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/images";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} of a single image.
+         */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/images";
+
+        /**
+         * Used in {@link Images#TYPE} column and indicats the row is a favicon.
+         */
+        public static final int IMAGE_TYPE_FAVICON = 1;
+
+        /**
+         * Used in {@link Images#TYPE} column and indicats the row is a precomposed touch icon.
+         */
+        public static final int IMAGE_TYPE_PRECOMPOSED_TOUCH_ICON = 2;
+
+        /**
+         * Used in {@link Images#TYPE} column and indicats the row is a touch icon.
+         */
+        public static final int IMAGE_TYPE_TOUCH_ICON = 4;
+
+        /**
+         * The type of item in the table.
+         * <P>Type: INTEGER</P>
+         * <p>Allowed values are:</p>
+         * <p>
+         * <ul>
+         * <li>{@link #IMAGE_TYPE_FAVICON}</li>
+         * <li>{@link #IMAGE_TYPE_PRECOMPOSED_TOUCH_ICON}</li>
+         * <li>{@link #IMAGE_TYPE_TOUCH_ICON}</li>
+         * </ul>
+         * </p>
+         */
+        public static final String TYPE = "type";
+
+        /**
+         * The image data.
+         * <p>Type: BLOB (image)</p>
+         */
+        public static final String DATA = "data";
+
+        /**
+         * The URL the images came from.
+         * <P>Type: TEXT (URL)</P>
+         * @hide
+         */
+        public static final String URL = "url_key";
+    }
+
+    /**
+     * <p>
+     * A table that stores the mappings between the image and the URL.
+     * </p>
+     * <p>
+     * Deleting or Updating a mapping might also deletes the mapped image if there is no other URL
+     * maps onto it.
+     * </p>
+     */
+    public static final class ImageMappings implements ImageMappingColumns {
+        /**
+         * This utility class cannot be instantiated
+         */
+        private ImageMappings() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "image_mappings");
+
+        /**
+         * The MIME type of {@link #CONTENT_URI} providing a directory of image mappings.
+         */
+        public static final String CONTENT_TYPE = "vnd.android.cursor.dir/image_mappings";
+
+        /**
+         * The MIME type of a {@link #CONTENT_URI} of a single image mapping.
+         */
+        public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/image_mappings";
+    }
+
+    /**
+     * A combined view of bookmarks and history. All bookmarks in all folders are included and
+     * no folders are included.
+     * @hide
+     */
+    public static final class Combined implements CommonColumns, HistoryColumns, ImageColumns {
+        /**
+         * This utility class cannot be instantiated
+         */
+        private Combined() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "combined");
+
+        /**
+         * Flag indicating that an item is a bookmark. A value of 1 indicates a bookmark, a value
+         * of 0 indicates a history item.
+         * <p>Type: INTEGER (boolean)</p>
+         */
+        public static final String IS_BOOKMARK = "bookmark";
+    }
+
+    /**
+     * A table that stores settings specific to the browser. Only support query and insert.
+     * @hide
+     */
+    public static final class Settings {
+        /**
+         * This utility class cannot be instantiated
+         */
+        private Settings() {}
+
+        /**
+         * The content:// style URI for this table
+         */
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(AUTHORITY_URI, "settings");
+
+        /**
+         * Key for a setting value.
+         */
+        public static final String KEY = "key";
+
+        /**
+         * Value for a setting.
+         */
+        public static final String VALUE = "value";
+
+        /**
+         * If set to non-0 the user has opted into bookmark sync.
+         */
+        public static final String KEY_SYNC_ENABLED = "sync_enabled";
+
+        /**
+         * Returns true if bookmark sync is enabled
+         */
+        static public boolean isSyncEnabled(Context context) {
+            Cursor cursor = null;
+            try {
+                cursor = context.getContentResolver().query(CONTENT_URI, new String[] { VALUE },
+                        KEY + "=?", new String[] { KEY_SYNC_ENABLED }, null);
+                if (cursor == null || !cursor.moveToFirst()) {
+                    return false;
+                }
+                return cursor.getInt(0) != 0;
+            } finally {
+                if (cursor != null) cursor.close();
+            }
+        }
+
+        /**
+         * Sets the bookmark sync enabled setting.
+         */
+        static public void setSyncEnabled(Context context, boolean enabled) {
+            ContentValues values = new ContentValues();
+            values.put(KEY, KEY_SYNC_ENABLED);
+            values.put(VALUE, enabled ? 1 : 0);
+            context.getContentResolver().insert(CONTENT_URI, values);
+        }
+    }
+}
diff --git a/src/com/android/browser/platformsupport/Process.java b/src/com/android/browser/platformsupport/Process.java
new file mode 100644
index 0000000..5731b27
--- /dev/null
+++ b/src/com/android/browser/platformsupport/Process.java
@@ -0,0 +1,48 @@
+/*
+ *  Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *      * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ *       copyright notice, this list of conditions and the following
+ *       disclaimer in the documentation and/or other materials provided
+ *       with the distribution.
+ *     * Neither the name of The Linux Foundation nor the names of its
+ *       contributors may be used to endorse or promote products derived
+ *       from this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ *  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ *  ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ *  BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ *  BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ *  WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ *  OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ *  IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package com.android.browser.platformsupport;
+
+public class Process {
+
+    public static final int PROC_SPACE_TERM = (int)' ';
+    public static final int PROC_COMBINE = 0x100;
+    public static final int PROC_OUT_LONG = 0x2000;
+
+
+    public static long getElapsedCpuTime() {
+        return 0;
+    }
+
+    public static boolean readProcFile(String s, int[] systemCpuFormat, Object o, long[]
+                                       sysCpu, Object o1) {
+        return false;
+    }
+}
diff --git a/src/com/android/browser/platformsupport/SeekBarPreference.java b/src/com/android/browser/platformsupport/SeekBarPreference.java
new file mode 100644
index 0000000..41b7915
--- /dev/null
+++ b/src/com/android/browser/platformsupport/SeekBarPreference.java
@@ -0,0 +1,231 @@
+package com.android.browser.platformsupport;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.Preference;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+public class SeekBarPreference extends Preference
+        implements OnSeekBarChangeListener {
+
+    private int mProgress;
+    private int mMax;
+    private boolean mTrackingTouch;
+
+    public SeekBarPreference(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        //SWE: Unable to attain the internal resources via reflection, instead
+        //attaining the max values from xml directly
+        int max = attrs.getAttributeIntValue(
+                      "http://schemas.android.com/apk/res/android", "max", mMax);
+        setMax(max);
+        setLayoutResource(com.android.browser.R.layout.preference_widget_seekbar);
+    }
+
+    public SeekBarPreference(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SeekBarPreference(Context context) {
+        this(context, null);
+    }
+
+    @Override
+    protected void onBindView(View view) {
+        super.onBindView(view);
+        SeekBar seekBar = (SeekBar) view.findViewById(
+                          com.android.browser.R.id.seekbar2);
+        seekBar.setOnSeekBarChangeListener(this);
+        seekBar.setMax(mMax);
+        seekBar.setProgress(mProgress);
+        seekBar.setEnabled(isEnabled());
+     }
+
+    @Override
+    public CharSequence getSummary() {
+        return null;
+    }
+
+    @Override
+    protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+        setProgress(restoreValue ? getPersistedInt(mProgress)
+                : (Integer) defaultValue);
+    }
+
+    @Override
+    protected Object onGetDefaultValue(TypedArray a, int index) {
+        return a.getInt(index, 0);
+    }
+
+    //@Override
+   public boolean onKey(View v, int keyCode, KeyEvent event) {
+        if (event.getAction() != KeyEvent.ACTION_UP) {
+            if (keyCode == KeyEvent.KEYCODE_PLUS
+                    || keyCode == KeyEvent.KEYCODE_EQUALS) {
+                setProgress(getProgress() + 1);
+                return true;
+            }
+            if (keyCode == KeyEvent.KEYCODE_MINUS) {
+                setProgress(getProgress() - 1);
+                return true;
+            }
+        }
+        return false;
+    }
+
+   public void setMax(int max) {
+        if (max != mMax) {
+            mMax = max;
+            notifyChanged();
+        }
+    }
+
+    public void setProgress(int progress) {
+        setProgress(progress, true);
+    }
+
+    private void setProgress(int progress, boolean notifyChanged) {
+        if (progress > mMax) {
+            progress = mMax;
+        }
+        if (progress < 0) {
+            progress = 0;
+        }
+        if (progress != mProgress) {
+            mProgress = progress;
+            persistInt(progress);
+            if (notifyChanged) {
+                notifyChanged();
+            }
+        }
+    }
+
+    public int getProgress() {
+        return mProgress;
+    }
+
+    /**
+     * Persist the seekBar's progress value if callChangeListener
+     * returns true, otherwise set the seekBar's progress to the stored value
+     */
+    void syncProgress(SeekBar seekBar) {
+        int progress = seekBar.getProgress();
+        if (progress != mProgress) {
+            if (callChangeListener(progress)) {
+                setProgress(progress, false);
+            } else {
+                seekBar.setProgress(mProgress);
+            }
+        }
+    }
+
+    @Override
+    public void onProgressChanged(
+            SeekBar seekBar, int progress, boolean fromUser) {
+        if (fromUser && !mTrackingTouch) {
+            syncProgress(seekBar);
+        }
+    }
+
+    @Override
+    public void onStartTrackingTouch(SeekBar seekBar) {
+        mTrackingTouch = true;
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar seekBar) {
+        mTrackingTouch = false;
+        if (seekBar.getProgress() != mProgress) {
+            syncProgress(seekBar);
+        }
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        /*
+         * Suppose a client uses this preference type without persisting. We
+         * must save the instance state so it is able to, for example, survive
+         * orientation changes.
+         */
+
+        final Parcelable superState = super.onSaveInstanceState();
+        if (isPersistent()) {
+            // No need to save instance state since it's persistent
+            return superState;
+        }
+
+        // Save the instance state
+        final SavedState myState = new SavedState(superState);
+        myState.progress = mProgress;
+        myState.max = mMax;
+        return myState;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (!state.getClass().equals(SavedState.class)) {
+            // Didn't save state for us in onSaveInstanceState
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        // Restore the instance state
+        SavedState myState = (SavedState) state;
+        super.onRestoreInstanceState(myState.getSuperState());
+        mProgress = myState.progress;
+        mMax = myState.max;
+        notifyChanged();
+    }
+
+    /**
+     * SavedState, a subclass of {@link BaseSavedState}, will store the state
+    * of MyPreference, a subclass of Preference.
+     * <p>
+     * It is important to always call through to super methods.
+     */
+    private static class SavedState extends BaseSavedState {
+        int progress;
+        int max;
+
+        public SavedState(Parcel source) {
+            super(source);
+
+            // Restore the click counter
+            progress = source.readInt();
+            max = source.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+
+            // Save the click counter
+            dest.writeInt(progress);
+            dest.writeInt(max);
+        }
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @SuppressWarnings("unused")
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+}
+
diff --git a/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java b/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java
new file mode 100644
index 0000000..d6a40c7
--- /dev/null
+++ b/src/com/android/browser/platformsupport/SyncStateContentProviderHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * Copyright (C) 2007 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.platformsupport;
+
+import android.accounts.Account;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.provider.SyncStateContract;
+
+/**
+ * Extends the schema of a ContentProvider to include the _sync_state table
+ * and implements query/insert/update/delete to access that table using the
+ * authority "syncstate". This can be used to store the sync state for a
+ * set of accounts.
+ */
+public class SyncStateContentProviderHelper {
+    private static final String SELECT_BY_ACCOUNT =
+            SyncStateContract.Columns.ACCOUNT_NAME + "=? AND "
+                    + SyncStateContract.Columns.ACCOUNT_TYPE + "=?";
+
+    private static final String SYNC_STATE_TABLE = "_sync_state";
+    private static final String SYNC_STATE_META_TABLE = "_sync_state_metadata";
+    private static final String SYNC_STATE_META_VERSION_COLUMN = "version";
+
+    private static long DB_VERSION = 1;
+
+    private static final String[] ACCOUNT_PROJECTION =
+            new String[]{SyncStateContract.Columns.ACCOUNT_NAME,
+                    SyncStateContract.Columns.ACCOUNT_TYPE};
+
+    public static final String PATH = "syncstate";
+
+    private static final String QUERY_COUNT_SYNC_STATE_ROWS =
+            "SELECT count(*)"
+                    + " FROM " + SYNC_STATE_TABLE
+                    + " WHERE " + SyncStateContract.Columns._ID + "=?";
+
+    public void createDatabase(SQLiteDatabase db) {
+        db.execSQL("DROP TABLE IF EXISTS " + SYNC_STATE_TABLE);
+        db.execSQL("CREATE TABLE " + SYNC_STATE_TABLE + " ("
+                + SyncStateContract.Columns._ID + " INTEGER PRIMARY KEY,"
+                + SyncStateContract.Columns.ACCOUNT_NAME + " TEXT NOT NULL,"
+                + SyncStateContract.Columns.ACCOUNT_TYPE + " TEXT NOT NULL,"
+                + SyncStateContract.Columns.DATA + " TEXT,"
+                + "UNIQUE(" + SyncStateContract.Columns.ACCOUNT_NAME + ", "
+                + SyncStateContract.Columns.ACCOUNT_TYPE + "));");
+
+        db.execSQL("DROP TABLE IF EXISTS " + SYNC_STATE_META_TABLE);
+        db.execSQL("CREATE TABLE " + SYNC_STATE_META_TABLE + " ("
+                + SYNC_STATE_META_VERSION_COLUMN + " INTEGER);");
+        ContentValues values = new ContentValues();
+        values.put(SYNC_STATE_META_VERSION_COLUMN, DB_VERSION);
+        db.insert(SYNC_STATE_META_TABLE, SYNC_STATE_META_VERSION_COLUMN, values);
+    }
+
+    public void onDatabaseOpened(SQLiteDatabase db) {
+        long version = DatabaseUtils.longForQuery(db,
+                "SELECT " + SYNC_STATE_META_VERSION_COLUMN + " FROM " + SYNC_STATE_META_TABLE,
+                null);
+        if (version != DB_VERSION) {
+            createDatabase(db);
+        }
+    }
+
+    public Cursor query(SQLiteDatabase db, String[] projection,
+                        String selection, String[] selectionArgs, String sortOrder) {
+        return db.query(SYNC_STATE_TABLE, projection, selection, selectionArgs,
+                null, null, sortOrder);
+    }
+
+    public long insert(SQLiteDatabase db, ContentValues values) {
+        return db.replace(SYNC_STATE_TABLE, SyncStateContract.Columns.ACCOUNT_NAME, values);
+    }
+
+    public int delete(SQLiteDatabase db, String userWhere, String[] whereArgs) {
+        return db.delete(SYNC_STATE_TABLE, userWhere, whereArgs);
+    }
+
+    public int update(SQLiteDatabase db, ContentValues values,
+                      String selection, String[] selectionArgs) {
+        return db.update(SYNC_STATE_TABLE, values, selection, selectionArgs);
+    }
+
+    public int update(SQLiteDatabase db, long rowId, Object data) {
+        if (DatabaseUtils.longForQuery(db, QUERY_COUNT_SYNC_STATE_ROWS,
+                new String[]{Long.toString(rowId)}) < 1) {
+            return 0;
+        }
+        db.execSQL("UPDATE " + SYNC_STATE_TABLE
+                + " SET " + SyncStateContract.Columns.DATA + "=?"
+                + " WHERE " + SyncStateContract.Columns._ID + "=" + rowId,
+                new Object[]{data});
+        // assume a row was modified since we know it exists
+        return 1;
+    }
+
+    public void onAccountsChanged(SQLiteDatabase db, Account[] accounts) {
+        Cursor c = db.query(SYNC_STATE_TABLE, ACCOUNT_PROJECTION, null, null, null, null, null);
+        try {
+            while (c.moveToNext()) {
+                final String accountName = c.getString(0);
+                final String accountType = c.getString(1);
+                Account account = new Account(accountName, accountType);
+                if (!contains(accounts, account)) {
+                    db.delete(SYNC_STATE_TABLE, SELECT_BY_ACCOUNT,
+                            new String[]{accountName, accountType});
+                }
+            }
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Checks that value is present as at least one of the elements of the array.
+     * @param array the array to check in
+     * @param value the value to check for
+     * @return true if the value is present in the array
+     */
+    private static <T> boolean contains(T[] array, T value) {
+        for (T element : array) {
+            if (element == null) {
+                if (value == null) return true;
+            } else {
+                if (value != null && element.equals(value)) return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/browser/platformsupport/WebAddress.java b/src/com/android/browser/platformsupport/WebAddress.java
new file mode 100644
index 0000000..10fac15
--- /dev/null
+++ b/src/com/android/browser/platformsupport/WebAddress.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2013 The Linux Foundation. All rights reserved.
+ * Not a contribution.
+ *
+ * Copyright (C) 2006 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.platformsupport;
+
+import static android.util.Patterns.GOOD_IRI_CHAR;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ *
+ * Web Address Parser
+ *
+ * This is called WebAddress, rather than URL or URI, because it
+ * attempts to parse the stuff that a user will actually type into a
+ * browser address widget.
+ *
+ * Unlike java.net.uri, this parser will not choke on URIs missing
+ * schemes.  It will only throw a ParseException if the input is
+ * really hosed.
+ *
+ * If given an https scheme but no port, fills in port
+ *
+ */
+public class WebAddress {
+
+    private String mScheme;
+    private String mHost;
+    private int mPort;
+    private String mPath;
+    private String mAuthInfo;
+
+    static final int MATCH_GROUP_SCHEME = 1;
+    static final int MATCH_GROUP_AUTHORITY = 2;
+    static final int MATCH_GROUP_HOST = 3;
+    static final int MATCH_GROUP_PORT = 4;
+    static final int MATCH_GROUP_PATH = 5;
+
+    /* ENRICO: imported the ParseExeption here */
+    public static class ParseException extends RuntimeException {
+        public String response;
+
+        ParseException(String response) {
+            this.response = response;
+        }
+    }
+
+    static Pattern sAddressPattern = Pattern.compile(
+            /* scheme    */ "(?:(http|https|file)\\:\\/\\/)?" +
+            /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
+            /* host      */ "([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" +
+            /* port      */ "(?:\\:([0-9]*))?" +
+            /* path      */ "(\\/?[^#]*)?" +
+            /* anchor    */ ".*", Pattern.CASE_INSENSITIVE);
+
+    /** parses given uriString. */
+    public WebAddress(String address) throws ParseException {
+        if (address == null) {
+            throw new NullPointerException();
+        }
+
+        // android.util.Log.d(LOGTAG, "WebAddress: " + address);
+
+        mScheme = "";
+        mHost = "";
+        mPort = -1;
+        mPath = "/";
+        mAuthInfo = "";
+
+        Matcher m = sAddressPattern.matcher(address);
+        String t;
+        if (m.matches()) {
+            t = m.group(MATCH_GROUP_SCHEME);
+            if (t != null) mScheme = t.toLowerCase();
+            t = m.group(MATCH_GROUP_AUTHORITY);
+            if (t != null) mAuthInfo = t;
+            t = m.group(MATCH_GROUP_HOST);
+            if (t != null) mHost = t;
+            t = m.group(MATCH_GROUP_PORT);
+            if (t != null && t.length() > 0) {
+                // The ':' character is not returned by the regex.
+                try {
+                    mPort = Integer.parseInt(t);
+                } catch (NumberFormatException ex) {
+                    throw new ParseException("Bad port");
+                }
+            }
+            t = m.group(MATCH_GROUP_PATH);
+            if (t != null && t.length() > 0) {
+                /* handle busted myspace frontpage redirect with
+                   missing initial "/" */
+                if (t.charAt(0) == '/') {
+                    mPath = t;
+                } else {
+                    mPath = "/" + t;
+                }
+            }
+
+        } else {
+            // nothing found... outa here
+            throw new ParseException("Bad address");
+        }
+
+        /* Get port from scheme or scheme from port, if necessary and
+           possible */
+        if (mPort == 443 && mScheme.equals("")) {
+            mScheme = "https";
+        } else if (mPort == -1) {
+            if (mScheme.equals("https"))
+                mPort = 443;
+            else
+                mPort = 80; // default
+        }
+        if (mScheme.equals("")) mScheme = "http";
+    }
+
+    @Override
+    public String toString() {
+        String port = "";
+        if ((mPort != 443 && mScheme.equals("https")) ||
+            (mPort != 80 && mScheme.equals("http"))) {
+            port = ":" + Integer.toString(mPort);
+        }
+        String authInfo = "";
+        if (mAuthInfo.length() > 0) {
+            authInfo = mAuthInfo + "@";
+        }
+
+        return mScheme + "://" + authInfo + mHost + port + mPath;
+    }
+
+    public void setScheme(String scheme) {
+      mScheme = scheme;
+    }
+
+    public String getScheme() {
+      return mScheme;
+    }
+
+    public void setHost(String host) {
+      mHost = host;
+    }
+
+    public String getHost() {
+      return mHost;
+    }
+
+    public void setPort(int port) {
+      mPort = port;
+    }
+
+    public int getPort() {
+      return mPort;
+    }
+
+    public void setPath(String path) {
+      mPath = path;
+    }
+
+    public String getPath() {
+      return mPath;
+    }
+
+    public void setAuthInfo(String authInfo) {
+      mAuthInfo = authInfo;
+    }
+
+    public String getAuthInfo() {
+      return mAuthInfo;
+    }
+}
diff --git a/src/com/android/browser/preferences/AboutPreferencesFragment.java b/src/com/android/browser/preferences/AboutPreferencesFragment.java
new file mode 100644
index 0000000..d979333
--- /dev/null
+++ b/src/com/android/browser/preferences/AboutPreferencesFragment.java
@@ -0,0 +1,60 @@
+/*
+    * Copyright (c) 2014, The Linux Foundation. All rights reserved.
+    *
+    * Redistribution and use in source and binary forms, with or without
+    * modification, are permitted provided that the following conditions are
+    * met:
+    * * Redistributions of source code must retain the above copyright
+    * notice, this list of conditions and the following disclaimer.
+    * * Redistributions in binary form must reproduce the above
+    * copyright notice, this list of conditions and the following
+    * disclaimer in the documentation and/or other materials provided
+    * with the distribution.
+    * * Neither the name of The Linux Foundation nor the names of its
+    * contributors may be used to endorse or promote products derived
+    * from this software without specific prior written permission.
+    *
+    * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+    * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+    * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+    * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+    * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+    * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+    * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+    * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+    * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+    * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+    * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+    *
+    */
+
+package com.android.browser.preferences;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.R;
+
+public class AboutPreferencesFragment extends PreferenceFragment
+                            implements OnPreferenceClickListener {
+
+    static final String PREF_ABOUT = "about_preference";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        addPreferencesFromResource(R.xml.about_preferences);
+        Preference aboutPreference = (Preference) findPreference(PREF_ABOUT);
+        String about_text = getString(R.string.about_text);
+        about_text = about_text.substring(about_text.indexOf("Hash"), about_text.length());
+        aboutPreference.setSummary(about_text);
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        return false;
+    }
+}
diff --git a/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java b/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java
new file mode 100644
index 0000000..529e388
--- /dev/null
+++ b/src/com/android/browser/preferences/AccessibilityPreferencesFragment.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import org.codeaurora.swe.WebView;
+import java.text.NumberFormat;
+
+public class AccessibilityPreferencesFragment extends PreferenceFragment
+        implements Preference.OnPreferenceChangeListener {
+
+    NumberFormat mFormat;
+    // Used to pause/resume timers, which are required for WebViewPreview
+    WebView mControlWebView;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mControlWebView = new WebView(getActivity());
+        addPreferencesFromResource(R.xml.accessibility_preferences);
+        BrowserSettings settings = BrowserSettings.getInstance();
+        mFormat = NumberFormat.getPercentInstance();
+
+        Preference e = findPreference(PreferenceKeys.PREF_MIN_FONT_SIZE);
+        e.setOnPreferenceChangeListener(this);
+        updateMinFontSummary(e, settings.getMinimumFontSize());
+        e = findPreference(PreferenceKeys.PREF_TEXT_ZOOM);
+        e.setOnPreferenceChangeListener(this);
+        updateTextZoomSummary(e, settings.getTextZoom());
+        e = findPreference(PreferenceKeys.PREF_DOUBLE_TAP_ZOOM);
+        e.setOnPreferenceChangeListener(this);
+        updateDoubleTapZoomSummary(e, settings.getDoubleTapZoom());
+        /*
+         * SWE_TODO: Commented out functionality for inverted rendering
+         * (as well as corresponding sections below)
+        e = findPreference(PreferenceKeys.PREF_INVERTED_CONTRAST);
+        e.setOnPreferenceChangeListener(this);
+        updateInvertedContrastSummary(e, (int) (settings.getInvertedContrast() * 100));
+        */
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mControlWebView.resumeTimers();
+    }
+
+    @Override
+    public void onPause() {
+        super.onPause();
+        mControlWebView.pauseTimers();
+    }
+
+    @Override
+    public void onDestroy() {
+        super.onDestroy();
+        mControlWebView.destroy();
+        mControlWebView = null;
+    }
+
+    void updateMinFontSummary(Preference pref, int minFontSize) {
+        Context c = getActivity();
+        pref.setSummary(c.getString(R.string.pref_min_font_size_value, minFontSize));
+    }
+
+    void updateTextZoomSummary(Preference pref, int textZoom) {
+        pref.setSummary(mFormat.format(textZoom / 100.0));
+    }
+
+    void updateDoubleTapZoomSummary(Preference pref, int doubleTapZoom) {
+        pref.setSummary(mFormat.format(doubleTapZoom / 100.0));
+    }
+
+    /*
+    void updateInvertedContrastSummary(Preference pref, int contrast) {
+        pref.setSummary(mFormat.format(contrast / 100.0));
+    }
+    */
+
+    @Override
+    public boolean onPreferenceChange(Preference pref, Object objValue) {
+        if (getActivity() == null) {
+            // We aren't attached, so don't accept preferences changes from the
+            // invisible UI.
+            return false;
+        }
+
+        if (PreferenceKeys.PREF_MIN_FONT_SIZE.equals(pref.getKey())) {
+            updateMinFontSummary(pref, BrowserSettings
+                    .getAdjustedMinimumFontSize((Integer) objValue));
+        }
+        if (PreferenceKeys.PREF_TEXT_ZOOM.equals(pref.getKey())) {
+            BrowserSettings settings = BrowserSettings.getInstance();
+            updateTextZoomSummary(pref, settings
+                    .getAdjustedTextZoom((Integer) objValue));
+        }
+        if (PreferenceKeys.PREF_DOUBLE_TAP_ZOOM.equals(pref.getKey())) {
+            BrowserSettings settings = BrowserSettings.getInstance();
+            updateDoubleTapZoomSummary(pref, settings
+                    .getAdjustedDoubleTapZoom((Integer) objValue));
+        }
+        /*
+        if (PreferenceKeys.PREF_INVERTED_CONTRAST.equals(pref.getKey())) {
+            updateInvertedContrastSummary(pref,
+                    (int) ((10 + (Integer) objValue) * 10));
+        }
+        */
+
+        return true;
+    }
+
+}
diff --git a/src/com/android/browser/preferences/AdvancedPreferencesFragment.java b/src/com/android/browser/preferences/AdvancedPreferencesFragment.java
new file mode 100644
index 0000000..4abd301
--- /dev/null
+++ b/src/com/android/browser/preferences/AdvancedPreferencesFragment.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2010 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.preferences;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.content.SharedPreferences.Editor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+import android.webkit.ValueCallback;
+import android.widget.Toast;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.BrowserSettings;
+import com.android.browser.DownloadHandler;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import java.util.Map;
+import java.util.Set;
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebStorage;
+
+public class AdvancedPreferencesFragment extends PreferenceFragment
+        implements Preference.OnPreferenceChangeListener {
+
+    private static final int DOWNLOAD_PATH_RESULT_CODE = 1;
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.advanced_preferences);
+
+        PreferenceScreen websiteSettings = (PreferenceScreen) findPreference(
+                PreferenceKeys.PREF_WEBSITE_SETTINGS);
+        websiteSettings.setFragment(WebsiteSettingsFragment.class.getName());
+
+        Preference e = findPreference(PreferenceKeys.PREF_DEFAULT_ZOOM);
+        e.setOnPreferenceChangeListener(this);
+        e.setSummary(getVisualDefaultZoomName(
+                getPreferenceScreen().getSharedPreferences()
+                .getString(PreferenceKeys.PREF_DEFAULT_ZOOM, null)) );
+
+        e = findPreference(PreferenceKeys.PREF_DEFAULT_TEXT_ENCODING);
+        e.setOnPreferenceChangeListener(this);
+
+        e = findPreference(PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES);
+        e.setOnPreferenceChangeListener(this);
+
+        e = findPreference(PreferenceKeys.PREF_SEARCH_ENGINE);
+        e.setOnPreferenceChangeListener(this);
+        updateListPreferenceSummary((ListPreference) e);
+
+        e = findPreference(PreferenceKeys.PREF_PLUGIN_STATE);
+        e.setOnPreferenceChangeListener(this);
+        updateListPreferenceSummary((ListPreference) e);
+        onInitdownloadSettingsPreference();
+    }
+
+    private void onInitdownloadSettingsPreference() {
+        addPreferencesFromResource(R.xml.download_settings_preferences);
+        PreferenceScreen downloadPathPreset =
+                (PreferenceScreen) findPreference(PreferenceKeys.PREF_DOWNLOAD_PATH);
+        downloadPathPreset.setOnPreferenceClickListener(onClickDownloadPathSettings());
+
+        String downloadPath = downloadPathPreset.getSharedPreferences().
+                getString(PreferenceKeys.PREF_DOWNLOAD_PATH,
+                        BrowserSettings.getInstance().getDownloadPath());
+        String downloadPathForUser = DownloadHandler.getDownloadPathForUser(this.getActivity(),
+                downloadPath);
+        downloadPathPreset.setSummary(downloadPathForUser);
+    }
+
+    private Preference.OnPreferenceClickListener onClickDownloadPathSettings() {
+        return new Preference.OnPreferenceClickListener() {
+            public boolean onPreferenceClick(Preference preference) {
+                try {
+                    Intent i = new Intent("com.android.fileexplorer.action.DIR_SEL");
+                    AdvancedPreferencesFragment.this.startActivityForResult(i,
+                            DOWNLOAD_PATH_RESULT_CODE);
+                } catch (Exception e) {
+                    String err_msg = getResources().getString(R.string.activity_not_found,
+                            "com.android.fileexplorer.action.DIR_SEL");
+                    Toast.makeText(getActivity(), err_msg, Toast.LENGTH_LONG).show();
+                }
+                return true;
+            }
+        };
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        super.onActivityResult(requestCode, resultCode, data);
+
+        if (requestCode == DOWNLOAD_PATH_RESULT_CODE) {
+            if (resultCode == Activity.RESULT_OK && data != null) {
+                String downloadPath = data.getStringExtra("result_dir_sel");
+                if (downloadPath != null) {
+                    PreferenceScreen downloadPathPreset =
+                            (PreferenceScreen) findPreference(PreferenceKeys.PREF_DOWNLOAD_PATH);
+                    Editor editor = downloadPathPreset.getEditor();
+                    editor.putString(PreferenceKeys.PREF_DOWNLOAD_PATH, downloadPath);
+                    editor.apply();
+                    String downloadPathForUser = DownloadHandler.getDownloadPathForUser(
+                            this.getActivity(), downloadPath);
+                    downloadPathPreset.setSummary(downloadPathForUser);
+                }
+
+                return;
+            }
+        }
+        return;
+    }
+
+    void updateListPreferenceSummary(ListPreference e) {
+        e.setSummary(e.getEntry());
+    }
+
+    /*
+     * We need to set the PreferenceScreen state in onResume(), as the number of
+     * origins with active features (WebStorage, Geolocation etc) could have
+     * changed after calling the WebsiteSettingsActivity.
+     */
+    @Override
+    public void onResume() {
+        super.onResume();
+        final PreferenceScreen websiteSettings = (PreferenceScreen) findPreference(
+                PreferenceKeys.PREF_WEBSITE_SETTINGS);
+        websiteSettings.setEnabled(false);
+        WebStorage.getInstance().getOrigins(new ValueCallback<Map>() {
+            @Override
+            public void onReceiveValue(Map webStorageOrigins) {
+                if ((webStorageOrigins != null) && !webStorageOrigins.isEmpty()) {
+                    websiteSettings.setEnabled(true);
+                }
+            }
+        });
+        GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() {
+            @Override
+            public void onReceiveValue(Set<String> geolocationOrigins) {
+                if ((geolocationOrigins != null) && !geolocationOrigins.isEmpty()) {
+                    websiteSettings.setEnabled(true);
+                }
+            }
+        });
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference pref, Object objValue) {
+        if (getActivity() == null) {
+            // We aren't attached, so don't accept preferences changes from the
+            // invisible UI.
+            Log.w("PageContentPreferencesFragment", "onPreferenceChange called from detached fragment!");
+            return false;
+        }
+
+        if (pref.getKey().equals(PreferenceKeys.PREF_DEFAULT_ZOOM)) {
+            pref.setSummary(getVisualDefaultZoomName((String) objValue));
+            return true;
+        } else if (pref.getKey().equals(PreferenceKeys.PREF_DEFAULT_TEXT_ENCODING)) {
+            pref.setSummary((String) objValue);
+            return true;
+        } else if (pref.getKey().equals(PreferenceKeys.PREF_RESET_DEFAULT_PREFERENCES)) {
+            Boolean value = (Boolean) objValue;
+            if (value.booleanValue() == true) {
+                startActivity(new Intent(BrowserActivity.ACTION_RESTART, null,
+                        getActivity(), BrowserActivity.class));
+                return true;
+            }
+        } else if (pref.getKey().equals(PreferenceKeys.PREF_PLUGIN_STATE)
+                || pref.getKey().equals(PreferenceKeys.PREF_SEARCH_ENGINE)) {
+            ListPreference lp = (ListPreference) pref;
+            lp.setValue((String) objValue);
+            updateListPreferenceSummary(lp);
+            return false;
+        }
+        return false;
+    }
+
+    private CharSequence getVisualDefaultZoomName(String enumName) {
+        Resources res = getActivity().getResources();
+        CharSequence[] visualNames = res.getTextArray(R.array.pref_default_zoom_choices);
+        CharSequence[] enumNames = res.getTextArray(R.array.pref_default_zoom_values);
+
+        // Sanity check
+        if (visualNames.length != enumNames.length) {
+            return "";
+        }
+
+        int length = enumNames.length;
+        for (int i = 0; i < length; i++) {
+            if (enumNames[i].equals(enumName)) {
+                return visualNames[i];
+            }
+        }
+
+        return "";
+    }
+}
diff --git a/src/com/android/browser/preferences/BandwidthPreferencesFragment.java b/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
new file mode 100644
index 0000000..0cb064a
--- /dev/null
+++ b/src/com/android/browser/preferences/BandwidthPreferencesFragment.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class BandwidthPreferencesFragment extends PreferenceFragment {
+
+    static final String TAG = "BandwidthPreferencesFragment";
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.bandwidth_preferences);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        PreferenceScreen prefScreen = getPreferenceScreen();
+        SharedPreferences sharedPrefs = prefScreen.getSharedPreferences();
+        if (!sharedPrefs.contains(PreferenceKeys.PREF_DATA_PRELOAD)) {
+            // set default value for preload setting
+            ListPreference preload = (ListPreference) prefScreen.findPreference(
+                    PreferenceKeys.PREF_DATA_PRELOAD);
+            if (preload != null) {
+                preload.setValue(BrowserSettings.getInstance().getDefaultPreloadSetting());
+            }
+        }
+        if (!sharedPrefs.contains(PreferenceKeys.PREF_LINK_PREFETCH)) {
+            // set default value for link prefetch setting
+            ListPreference prefetch = (ListPreference) prefScreen.findPreference(
+                    PreferenceKeys.PREF_LINK_PREFETCH);
+            if (prefetch != null) {
+                prefetch.setValue(BrowserSettings.getInstance().getDefaultLinkPrefetchSetting());
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/browser/preferences/DebugPreferencesFragment.java b/src/com/android/browser/preferences/DebugPreferencesFragment.java
new file mode 100644
index 0000000..24821d1
--- /dev/null
+++ b/src/com/android/browser/preferences/DebugPreferencesFragment.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2010 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.preferences;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.GoogleAccountLogin;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+public class DebugPreferencesFragment extends PreferenceFragment
+        implements OnPreferenceClickListener {
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.debug_preferences);
+
+        Preference e = findPreference(PreferenceKeys.PREF_RESET_PRELOGIN);
+        e.setOnPreferenceClickListener(this);
+    }
+
+    @Override
+    public boolean onPreferenceClick(Preference preference) {
+        if (PreferenceKeys.PREF_RESET_PRELOGIN.equals(preference.getKey())) {
+            BrowserSettings.getInstance().getPreferences().edit()
+                    .remove(GoogleAccountLogin.PREF_AUTOLOGIN_TIME)
+                    .apply();
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/src/com/android/browser/preferences/FontSizePreview.java b/src/com/android/browser/preferences/FontSizePreview.java
new file mode 100644
index 0000000..e9d69d5
--- /dev/null
+++ b/src/com/android/browser/preferences/FontSizePreview.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.AttributeSet;
+import android.view.View;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+
+public class FontSizePreview extends WebViewPreview {
+
+    static final String HTML_FORMAT = "<!DOCTYPE html><html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\"><style type=\"text/css\">p { margin: 2px auto;}</style><body><p style=\"font-size: 4pt\">%s</p><p style=\"font-size: 8pt\">%s</p><p style=\"font-size: 10pt\">%s</p><p style=\"font-size: 14pt\">%s</p><p style=\"font-size: 18pt\">%s</p></body></html>";
+
+    String mHtml;
+
+    public FontSizePreview(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public FontSizePreview(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public FontSizePreview(Context context) {
+        super(context);
+    }
+
+    @Override
+    protected void init(Context context) {
+        super.init(context);
+        Resources res = context.getResources();
+        Object[] visualNames = res.getStringArray(R.array.pref_text_size_choices);
+        mHtml = String.format(HTML_FORMAT, visualNames);
+    }
+
+    @Override
+    protected void updatePreview(boolean forceReload) {
+        if (mWebView == null) return;
+
+        WebSettings ws = mWebView.getSettings();
+        BrowserSettings bs = BrowserSettings.getInstance();
+        ws.setMinimumFontSize(bs.getMinimumFontSize());
+        ws.setTextZoom(bs.getTextZoom());
+        mWebView.loadDataWithBaseURL(null, mHtml, "text/html", "utf-8", null);
+    }
+
+    @Override
+    protected void setupWebView(WebView view) {
+        super.setupWebView(view);
+        view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
+    }
+
+}
diff --git a/src/com/android/browser/preferences/GeneralPreferencesFragment.java b/src/com/android/browser/preferences/GeneralPreferencesFragment.java
new file mode 100644
index 0000000..2453f46
--- /dev/null
+++ b/src/com/android/browser/preferences/GeneralPreferencesFragment.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright (C) 2010 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.preferences;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.res.Resources;
+import android.os.Bundle;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.TextView;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.browser.BrowserPreferencesPage;
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+import com.android.browser.homepages.HomeProvider;
+
+public class GeneralPreferencesFragment extends PreferenceFragment
+        implements Preference.OnPreferenceChangeListener {
+
+    static final String TAG = "PersonalPreferencesFragment";
+
+    static final String BLANK_URL = "about:blank";
+    static final String CURRENT = "current";
+    static final String BLANK = "blank";
+    static final String DEFAULT = "default";
+    static final String MOST_VISITED = "most_visited";
+    static final String OTHER = "other";
+
+    static final String PREF_HOMEPAGE_PICKER = "homepage_picker";
+
+    String[] mChoices, mValues;
+    String mCurrentPage;
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        Resources res = getActivity().getResources();
+        mChoices = res.getStringArray(R.array.pref_homepage_choices);
+        mValues = res.getStringArray(R.array.pref_homepage_values);
+        mCurrentPage = getActivity().getIntent()
+                .getStringExtra(BrowserPreferencesPage.CURRENT_PAGE);
+
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.general_preferences);
+
+        ListPreference pref = (ListPreference) findPreference(PREF_HOMEPAGE_PICKER);
+        pref.setSummary(getHomepageSummary());
+        pref.setPersistent(false);
+        pref.setValue(getHomepageValue());
+        pref.setOnPreferenceChangeListener(this);
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference pref, Object objValue) {
+        if (getActivity() == null) {
+            // We aren't attached, so don't accept preferences changes from the
+            // invisible UI.
+            Log.w("PageContentPreferencesFragment", "onPreferenceChange called from detached fragment!");
+            return false;
+        }
+
+        if (pref.getKey().equals(PREF_HOMEPAGE_PICKER)) {
+            BrowserSettings settings = BrowserSettings.getInstance();
+            if (CURRENT.equals(objValue)) {
+                settings.setHomePage(mCurrentPage);
+            }
+            if (BLANK.equals(objValue)) {
+                settings.setHomePage(BLANK_URL);
+            }
+            if (DEFAULT.equals(objValue)) {
+                settings.setHomePage(BrowserSettings.getFactoryResetHomeUrl(
+                        getActivity()));
+            }
+            if (MOST_VISITED.equals(objValue)) {
+                settings.setHomePage(HomeProvider.MOST_VISITED);
+            }
+            if (OTHER.equals(objValue)) {
+                promptForHomepage();
+                return false;
+            }
+            pref.setSummary(getHomepageSummary());
+            ((ListPreference)pref).setValue(getHomepageValue());
+            return false;
+        }
+
+        return true;
+    }
+
+    void promptForHomepage() {
+        MyAlertDialogFragment fragment = MyAlertDialogFragment.newInstance();
+        fragment.setTargetFragment(this, -1);
+        fragment.show(getActivity().getFragmentManager(), "setHomepage dialog");
+    }
+
+    String getHomepageValue() {
+        BrowserSettings settings = BrowserSettings.getInstance();
+        String homepage = settings.getHomePage();
+        if (TextUtils.isEmpty(homepage) || BLANK_URL.endsWith(homepage)) {
+            return BLANK;
+        }
+        if (HomeProvider.MOST_VISITED.equals(homepage)) {
+            return MOST_VISITED;
+        }
+        String defaultHomepage = BrowserSettings.getFactoryResetHomeUrl(
+                getActivity());
+        if (TextUtils.equals(defaultHomepage, homepage)) {
+            return DEFAULT;
+        }
+        if (TextUtils.equals(mCurrentPage, homepage)) {
+            return CURRENT;
+        }
+        return OTHER;
+    }
+
+    String getHomepageSummary() {
+        BrowserSettings settings = BrowserSettings.getInstance();
+        if (settings.useMostVisitedHomepage()) {
+            return getHomepageLabel(MOST_VISITED);
+        }
+        String homepage = settings.getHomePage();
+        if (TextUtils.isEmpty(homepage) || BLANK_URL.equals(homepage)) {
+            return getHomepageLabel(BLANK);
+        }
+        return homepage;
+    }
+
+    String getHomepageLabel(String value) {
+        for (int i = 0; i < mValues.length; i++) {
+            if (value.equals(mValues[i])) {
+                return mChoices[i];
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+
+        refreshUi();
+    }
+
+    void refreshUi() {
+        PreferenceScreen autoFillSettings =
+                (PreferenceScreen)findPreference(PreferenceKeys.PREF_AUTOFILL_PROFILE);
+        autoFillSettings.setDependency(PreferenceKeys.PREF_AUTOFILL_ENABLED);
+    }
+
+    /*
+      Add this class to manage AlertDialog lifecycle.
+    */
+    public static class MyAlertDialogFragment extends DialogFragment {
+        public static MyAlertDialogFragment newInstance() {
+            MyAlertDialogFragment frag = new MyAlertDialogFragment();
+            return frag;
+        }
+
+        @Override
+        public Dialog onCreateDialog(Bundle savedInstanceState) {
+            final BrowserSettings settings = BrowserSettings.getInstance();
+            final EditText editText = new EditText(getActivity());
+            editText.setInputType(InputType.TYPE_CLASS_TEXT
+                    | InputType.TYPE_TEXT_VARIATION_URI);
+            editText.setText(settings.getHomePage());
+            editText.setSelectAllOnFocus(true);
+            editText.setSingleLine(true);
+            editText.setImeActionLabel(null, EditorInfo.IME_ACTION_DONE);
+            final AlertDialog dialog = new AlertDialog.Builder(getActivity())
+                    .setView(editText)
+                    .setPositiveButton(android.R.string.ok, new OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            String homepage = editText.getText().toString().trim();
+                            homepage = UrlUtils.smartUrlFilter(homepage);
+                            settings.setHomePage(homepage);
+                            Fragment frag = getTargetFragment();
+                            if (frag == null || !(frag instanceof GeneralPreferencesFragment)) {
+                                Log.e("MyAlertDialogFragment", "get target fragment error!");
+                                return;
+                            }
+                            GeneralPreferencesFragment target = (GeneralPreferencesFragment)frag;
+                            ListPreference pref = (ListPreference) target.
+                                    findPreference(PREF_HOMEPAGE_PICKER);
+                            pref.setValue(target.getHomepageValue());
+                            pref.setSummary(target.getHomepageSummary());
+                        }
+                    })
+                    .setNegativeButton(android.R.string.cancel, new OnClickListener() {
+                        @Override
+                        public void onClick(DialogInterface dialog, int which) {
+                            dialog.cancel();
+                        }
+                    })
+                    .setTitle(R.string.pref_set_homepage_to)
+                    .create();
+
+            editText.setOnEditorActionListener(new OnEditorActionListener() {
+                @Override
+                public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+                    if (actionId == EditorInfo.IME_ACTION_DONE) {
+                        dialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
+                        return true;
+                    }
+                    return false;
+                }
+            });
+
+            dialog.getWindow().setSoftInputMode(
+                    WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
+
+            return dialog;
+        }
+    }
+}
diff --git a/src/com/android/browser/preferences/InvertedContrastPreview.java b/src/com/android/browser/preferences/InvertedContrastPreview.java
new file mode 100644
index 0000000..8064c30
--- /dev/null
+++ b/src/com/android/browser/preferences/InvertedContrastPreview.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import org.codeaurora.swe.WebSettings;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.BrowserWebView;
+import com.android.browser.WebViewProperties;
+
+public class InvertedContrastPreview extends WebViewPreview {
+
+    static final String IMG_ROOT = "content://com.android.browser.home/res/raw/";
+    static final String[] THUMBS = new String[] {
+        "thumb_google",
+        "thumb_amazon",
+        "thumb_cnn",
+        "thumb_espn",
+        "", // break
+        "thumb_bbc",
+        "thumb_nytimes",
+        "thumb_weatherchannel",
+        "thumb_picasa",
+    };
+
+    String mHtml;
+
+    public InvertedContrastPreview(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public InvertedContrastPreview(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public InvertedContrastPreview(Context context) {
+        super(context);
+    }
+    
+    @Override
+    protected void init(Context context) {
+        super.init(context);
+        StringBuilder builder = new StringBuilder("<html><body style=\"width: 1000px\">");
+        for (String thumb : THUMBS) {
+            if (TextUtils.isEmpty(thumb)) {
+                builder.append("<br />");
+                continue;
+            }
+            builder.append("<img src=\"");
+            builder.append(IMG_ROOT);
+            builder.append(thumb);
+            builder.append("\" />&nbsp;");
+        }
+        builder.append("</body></html>");
+        mHtml = builder.toString();
+    }
+
+    @Override
+    protected void updatePreview(boolean forceReload) {
+        if (mWebView == null) return;
+
+        WebSettings ws = mWebView.getSettings();
+        BrowserSettings bs = BrowserSettings.getInstance();
+        ws.setProperty(WebViewProperties.gfxInvertedScreen,
+                bs.useInvertedRendering() ? "true" : "false");
+        ws.setProperty(WebViewProperties.gfxInvertedScreenContrast,
+                Float.toString(bs.getInvertedContrast()));
+        if (forceReload) {
+            mWebView.loadData(mHtml, "text/html", null);
+        }
+    }
+
+}
diff --git a/src/com/android/browser/preferences/LabPreferencesFragment.java b/src/com/android/browser/preferences/LabPreferencesFragment.java
new file mode 100644
index 0000000..222b5fa
--- /dev/null
+++ b/src/com/android/browser/preferences/LabPreferencesFragment.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2010 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.preferences;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+import com.android.browser.search.SearchEngine;
+
+public class LabPreferencesFragment extends PreferenceFragment {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        // Load the XML preferences file
+        addPreferencesFromResource(R.xml.lab_preferences);
+    }
+}
diff --git a/src/com/android/browser/preferences/NonformattingListPreference.java b/src/com/android/browser/preferences/NonformattingListPreference.java
new file mode 100644
index 0000000..51b3231
--- /dev/null
+++ b/src/com/android/browser/preferences/NonformattingListPreference.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.Context;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+
+public class NonformattingListPreference extends ListPreference {
+
+    private CharSequence mSummary;
+
+    public NonformattingListPreference(Context context) {
+        super(context);
+    }
+
+    public NonformattingListPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    public void setSummary(CharSequence summary) {
+        mSummary = summary;
+        super.setSummary(summary);
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        if (mSummary != null) {
+            return mSummary;
+        }
+        return super.getSummary();
+    }
+
+}
diff --git a/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java b/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java
new file mode 100644
index 0000000..35e6e43
--- /dev/null
+++ b/src/com/android/browser/preferences/PrivacySecurityPreferencesFragment.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2010 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.preferences;
+
+import com.android.browser.PreferenceKeys;
+import com.android.browser.R;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+
+public class PrivacySecurityPreferencesFragment extends PreferenceFragment
+        implements Preference.OnPreferenceChangeListener {
+
+    @Override
+    public void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        // Load the preferences from an XML resource
+        addPreferencesFromResource(R.xml.privacy_security_preferences);
+
+        Preference e = findPreference(PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY);
+        e.setOnPreferenceChangeListener(this);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+    }
+
+    @Override
+    public boolean onPreferenceChange(Preference pref, Object objValue) {
+        if (pref.getKey().equals(PreferenceKeys.PREF_PRIVACY_CLEAR_HISTORY)
+                && ((Boolean) objValue).booleanValue() == true) {
+            // Need to tell the browser to remove the parent/child relationship
+            // between tabs
+            getActivity().setResult(Activity.RESULT_OK, (new Intent()).putExtra(Intent.EXTRA_TEXT,
+                    pref.getKey()));
+            return true;
+        }
+
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/preferences/SeekBarSummaryPreference.java b/src/com/android/browser/preferences/SeekBarSummaryPreference.java
new file mode 100644
index 0000000..5cb8ae6
--- /dev/null
+++ b/src/com/android/browser/preferences/SeekBarSummaryPreference.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.Context;
+
+import com.android.browser.platformsupport.SeekBarPreference;
+
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+
+public class SeekBarSummaryPreference extends SeekBarPreference {
+
+    CharSequence mSummary;
+    TextView mSummaryView;
+
+    public SeekBarSummaryPreference(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    public SeekBarSummaryPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public SeekBarSummaryPreference(Context context) {
+        super(context);
+        init();
+    }
+
+    void init() {
+        setWidgetLayoutResource(com.android.browser.R.layout.font_size_widget);
+    }
+
+    @Override
+    public void setSummary(CharSequence summary) {
+        mSummary = summary;
+        if (mSummaryView != null) {
+            mSummaryView.setText(mSummary);
+        }
+    }
+
+    @Override
+    public CharSequence getSummary() {
+        return null;
+    }
+
+    @Override
+    protected void onBindView(View view) {
+        super.onBindView(view);
+        mSummaryView = (TextView) view.findViewById(com.android.browser.R.id.text);
+        if (TextUtils.isEmpty(mSummary)) {
+            mSummaryView.setVisibility(View.GONE);
+        } else {
+            mSummaryView.setVisibility(View.VISIBLE);
+            mSummaryView.setText(mSummary);
+        }
+    }
+
+    @Override
+    public void onStartTrackingTouch(SeekBar seekBar) {
+        // Intentionally blank - prevent super.onStartTrackingTouch from running
+    }
+
+    @Override
+    public void onStopTrackingTouch(SeekBar seekBar) {
+        // Intentionally blank - prevent onStopTrackingTouch from running
+    }
+
+}
diff --git a/src/com/android/browser/preferences/WebViewPreview.java b/src/com/android/browser/preferences/WebViewPreview.java
new file mode 100644
index 0000000..ce24ac3
--- /dev/null
+++ b/src/com/android/browser/preferences/WebViewPreview.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 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.preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
+import android.preference.Preference;
+import android.preference.PreferenceManager;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import org.codeaurora.swe.WebView;
+
+import com.android.browser.R;
+
+public abstract class WebViewPreview extends Preference
+        implements OnSharedPreferenceChangeListener {
+
+    protected WebView mWebView;
+
+    public WebViewPreview(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    public WebViewPreview(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public WebViewPreview(Context context) {
+        super(context);
+        init(context);
+    }
+
+    protected void init(Context context) {
+        setLayoutResource(R.layout.webview_preview);
+    }
+
+    protected abstract void updatePreview(boolean forceReload);
+
+    protected void setupWebView(WebView view) {}
+
+    @Override
+    protected View onCreateView(ViewGroup parent) {
+        View root = super.onCreateView(parent);
+        WebView webView = (WebView) root.findViewById(R.id.webview);
+        // Tell WebView to really, truly ignore all touch events. No, seriously,
+        // ignore them all. And don't show scrollbars.
+        webView.setFocusable(false);
+        webView.setFocusableInTouchMode(false);
+        webView.setClickable(false);
+        webView.setLongClickable(false);
+        webView.setHorizontalScrollBarEnabled(false);
+        webView.setVerticalScrollBarEnabled(false);
+        setupWebView(webView);
+        return root;
+    }
+
+    @Override
+    protected void onBindView(View view) {
+        super.onBindView(view);
+        mWebView = (WebView) view.findViewById(R.id.webview);
+        updatePreview(true);
+    }
+
+    @Override
+    protected void onAttachedToHierarchy(PreferenceManager preferenceManager) {
+        super.onAttachedToHierarchy(preferenceManager);
+        getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
+    }
+
+    @Override
+    protected void onPrepareForRemoval() {
+        getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
+        super.onPrepareForRemoval();
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(
+            SharedPreferences sharedPreferences, String key) {
+        updatePreview(false);
+    }
+
+}
diff --git a/src/com/android/browser/preferences/WebsiteSettingsFragment.java b/src/com/android/browser/preferences/WebsiteSettingsFragment.java
new file mode 100644
index 0000000..a621dec
--- /dev/null
+++ b/src/com/android/browser/preferences/WebsiteSettingsFragment.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2009 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.preferences;
+
+import android.app.AlertDialog;
+import android.app.ListFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.webkit.ValueCallback;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.browser.R;
+import com.android.browser.WebStorageSizeManager;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import org.codeaurora.swe.GeolocationPermissions;
+import org.codeaurora.swe.WebStorage;
+
+/**
+ * Manage the settings for an origin.
+ * We use it to keep track of the 'HTML5' settings, i.e. database (webstorage)
+ * and Geolocation.
+ */
+public class WebsiteSettingsFragment extends ListFragment implements OnClickListener {
+
+    private static final String EXTRA_SITE = "site";
+    private String LOGTAG = "WebsiteSettingsActivity";
+    private static String sMBStored = null;
+    private SiteAdapter mAdapter = null;
+    private Site mSite = null;
+
+    static class Site implements Parcelable {
+        private String mOrigin;
+        private String mTitle;
+        private Bitmap mIcon;
+        private int mFeatures;
+
+        // These constants provide the set of features that a site may support
+        // They must be consecutive. To add a new feature, add a new FEATURE_XXX
+        // variable with value equal to the current value of FEATURE_COUNT, then
+        // increment FEATURE_COUNT.
+        final static int FEATURE_WEB_STORAGE = 0;
+        final static int FEATURE_GEOLOCATION = 1;
+        // The number of features available.
+        final static int FEATURE_COUNT = 2;
+
+        public Site(String origin) {
+            mOrigin = origin;
+            mTitle = null;
+            mIcon = null;
+            mFeatures = 0;
+        }
+
+        public void addFeature(int feature) {
+            mFeatures |= (1 << feature);
+        }
+
+        public void removeFeature(int feature) {
+            mFeatures &= ~(1 << feature);
+        }
+
+        public boolean hasFeature(int feature) {
+            return (mFeatures & (1 << feature)) != 0;
+        }
+
+        /**
+         * Gets the number of features supported by this site.
+         */
+        public int getFeatureCount() {
+            int count = 0;
+            for (int i = 0; i < FEATURE_COUNT; ++i) {
+                count += hasFeature(i) ? 1 : 0;
+            }
+            return count;
+        }
+
+        /**
+         * Gets the ID of the nth (zero-based) feature supported by this site.
+         * The return value is a feature ID - one of the FEATURE_XXX values.
+         * This is required to determine which feature is displayed at a given
+         * position in the list of features for this site. This is used both
+         * when populating the view and when responding to clicks on the list.
+         */
+        public int getFeatureByIndex(int n) {
+            int j = -1;
+            for (int i = 0; i < FEATURE_COUNT; ++i) {
+                j += hasFeature(i) ? 1 : 0;
+                if (j == n) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        public String getOrigin() {
+            return mOrigin;
+        }
+
+        public void setTitle(String title) {
+            mTitle = title;
+        }
+
+        public void setIcon(Bitmap icon) {
+            mIcon = icon;
+        }
+
+        public Bitmap getIcon() {
+            return mIcon;
+        }
+
+        public String getPrettyOrigin() {
+            return mTitle == null ? null : hideHttp(mOrigin);
+        }
+
+        public String getPrettyTitle() {
+            return mTitle == null ? hideHttp(mOrigin) : mTitle;
+        }
+
+        private String hideHttp(String str) {
+            Uri uri = Uri.parse(str);
+            return "http".equals(uri.getScheme()) ?  str.substring(7) : str;
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mOrigin);
+            dest.writeString(mTitle);
+            dest.writeInt(mFeatures);
+            dest.writeParcelable(mIcon, flags);
+        }
+
+        private Site(Parcel in) {
+            mOrigin = in.readString();
+            mTitle = in.readString();
+            mFeatures = in.readInt();
+            mIcon = in.readParcelable(null);
+        }
+
+        public static final Parcelable.Creator<Site> CREATOR
+                = new Parcelable.Creator<Site>() {
+            public Site createFromParcel(Parcel in) {
+                return new Site(in);
+            }
+
+            public Site[] newArray(int size) {
+                return new Site[size];
+            }
+        };
+
+    }
+
+    class SiteAdapter extends ArrayAdapter<Site>
+            implements AdapterView.OnItemClickListener {
+        private int mResource;
+        private LayoutInflater mInflater;
+        private Bitmap mDefaultIcon;
+        private Bitmap mUsageEmptyIcon;
+        private Bitmap mUsageLowIcon;
+        private Bitmap mUsageHighIcon;
+        private Bitmap mLocationAllowedIcon;
+        private Bitmap mLocationDisallowedIcon;
+        private Site mCurrentSite;
+
+        public SiteAdapter(Context context, int rsc) {
+            this(context, rsc, null);
+        }
+
+        public SiteAdapter(Context context, int rsc, Site site) {
+            super(context, rsc);
+            mResource = rsc;
+            mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            mDefaultIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.app_web_browser_sm);
+            mUsageEmptyIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.ic_list_data_off);
+            mUsageLowIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.ic_list_data_small);
+            mUsageHighIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.ic_list_data_large);
+            mLocationAllowedIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.ic_gps_on_holo_dark);
+            mLocationDisallowedIcon = BitmapFactory.decodeResource(getResources(),
+                    R.drawable.ic_gps_denied_holo_dark);
+            mCurrentSite = site;
+            if (mCurrentSite == null) {
+                askForOrigins();
+            }
+        }
+
+        /**
+         * Adds the specified feature to the site corresponding to supplied
+         * origin in the map. Creates the site if it does not already exist.
+         */
+        private void addFeatureToSite(Map<String, Site> sites, String origin, int feature) {
+            Site site = null;
+            if (sites.containsKey(origin)) {
+                site = (Site) sites.get(origin);
+            } else {
+                site = new Site(origin);
+                sites.put(origin, site);
+            }
+            site.addFeature(feature);
+        }
+
+        public void askForOrigins() {
+            // Get the list of origins we want to display.
+            // All 'HTML 5 modules' (Database, Geolocation etc) form these
+            // origin strings using WebCore::SecurityOrigin::toString(), so it's
+            // safe to group origins here. Note that WebCore::SecurityOrigin
+            // uses 0 (which is not printed) for the port if the port is the
+            // default for the protocol. Eg http://www.google.com and
+            // http://www.google.com:80 both record a port of 0 and hence
+            // toString() == 'http://www.google.com' for both.
+
+            WebStorage.getInstance().getOrigins(new ValueCallback<Map>() {
+                public void onReceiveValue(Map origins) {
+                    Map<String, Site> sites = new HashMap<String, Site>();
+                    if (origins != null) {
+                        Iterator<String> iter = origins.keySet().iterator();
+                        while (iter.hasNext()) {
+                            addFeatureToSite(sites, iter.next(), Site.FEATURE_WEB_STORAGE);
+                        }
+                    }
+                    askForGeolocation(sites);
+                }
+            });
+        }
+
+        public void askForGeolocation(final Map<String, Site> sites) {
+            GeolocationPermissions.getInstance().getOrigins(new ValueCallback<Set<String> >() {
+                public void onReceiveValue(Set<String> origins) {
+                    if (origins != null) {
+                        Iterator<String> iter = origins.iterator();
+                        while (iter.hasNext()) {
+                            addFeatureToSite(sites, iter.next(), Site.FEATURE_GEOLOCATION);
+                        }
+                    }
+                    populateIcons(sites);
+                    populateOrigins(sites);
+                }
+            });
+        }
+
+        public void populateIcons(Map<String, Site> sites) {
+            // Create a map from host to origin. This is used to add metadata
+            // (title, icon) for this origin from the bookmarks DB. We must do
+            // the DB access on a background thread.
+            new UpdateFromBookmarksDbTask(this.getContext(), sites).execute();
+        }
+
+        private class UpdateFromBookmarksDbTask extends AsyncTask<Void, Void, Void> {
+
+            private Context mContext;
+            private boolean mDataSetChanged;
+            private Map<String, Site> mSites;
+
+            public UpdateFromBookmarksDbTask(Context ctx, Map<String, Site> sites) {
+                mContext = ctx.getApplicationContext();
+                mSites = sites;
+            }
+
+            protected Void doInBackground(Void... unused) {
+                HashMap<String, Set<Site>> hosts = new HashMap<String, Set<Site>>();
+                Set<Map.Entry<String, Site>> elements = mSites.entrySet();
+                Iterator<Map.Entry<String, Site>> originIter = elements.iterator();
+                while (originIter.hasNext()) {
+                    Map.Entry<String, Site> entry = originIter.next();
+                    Site site = entry.getValue();
+                    String host = Uri.parse(entry.getKey()).getHost();
+                    Set<Site> hostSites = null;
+                    if (hosts.containsKey(host)) {
+                        hostSites = (Set<Site>)hosts.get(host);
+                    } else {
+                        hostSites = new HashSet<Site>();
+                        hosts.put(host, hostSites);
+                    }
+                    hostSites.add(site);
+                }
+
+                // Check the bookmark DB. If we have data for a host used by any of
+                // our origins, use it to set their title and favicon
+                Cursor c = mContext.getContentResolver().query(Bookmarks.CONTENT_URI,
+                        new String[] { Bookmarks.URL, Bookmarks.TITLE, Bookmarks.FAVICON },
+                        Bookmarks.IS_FOLDER + " == 0", null, null);
+
+                if (c != null) {
+                    if (c.moveToFirst()) {
+                        int urlIndex = c.getColumnIndex(Bookmarks.URL);
+                        int titleIndex = c.getColumnIndex(Bookmarks.TITLE);
+                        int faviconIndex = c.getColumnIndex(Bookmarks.FAVICON);
+                        do {
+                            String url = c.getString(urlIndex);
+                            String host = Uri.parse(url).getHost();
+                            if (hosts.containsKey(host)) {
+                                String title = c.getString(titleIndex);
+                                Bitmap bmp = null;
+                                byte[] data = c.getBlob(faviconIndex);
+                                if (data != null) {
+                                    bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
+                                }
+                                Set matchingSites = (Set) hosts.get(host);
+                                Iterator<Site> sitesIter = matchingSites.iterator();
+                                while (sitesIter.hasNext()) {
+                                    Site site = sitesIter.next();
+                                    // We should only set the title if the bookmark is for the root
+                                    // (i.e. www.google.com), as website settings act on the origin
+                                    // as a whole rather than a single page under that origin. If the
+                                    // user has bookmarked a page under the root but *not* the root,
+                                    // then we risk displaying the title of that page which may or
+                                    // may not have any relevance to the origin.
+                                    if (url.equals(site.getOrigin()) ||
+                                            (new String(site.getOrigin()+"/")).equals(url)) {
+                                        mDataSetChanged = true;
+                                        site.setTitle(title);
+                                    }
+
+                                    if (bmp != null) {
+                                        mDataSetChanged = true;
+                                        site.setIcon(bmp);
+                                    }
+                                }
+                            }
+                        } while (c.moveToNext());
+                    }
+                    c.close();
+                }
+            return null;
+            }
+
+            protected void onPostExecute(Void unused) {
+                if (mDataSetChanged) {
+                    notifyDataSetChanged();
+                }
+            }
+        }
+
+
+        public void populateOrigins(Map<String, Site> sites) {
+            clear();
+
+            // We can now simply populate our array with Site instances
+            Set<Map.Entry<String, Site>> elements = sites.entrySet();
+            Iterator<Map.Entry<String, Site>> entryIterator = elements.iterator();
+            while (entryIterator.hasNext()) {
+                Map.Entry<String, Site> entry = entryIterator.next();
+                Site site = entry.getValue();
+                add(site);
+            }
+
+            notifyDataSetChanged();
+
+            if (getCount() == 0) {
+                finish(); // we close the screen
+            }
+        }
+
+        public int getCount() {
+            if (mCurrentSite == null) {
+                return super.getCount();
+            }
+            return mCurrentSite.getFeatureCount();
+        }
+
+        public String sizeValueToString(long bytes) {
+            // We display the size in MB, to 1dp, rounding up to the next 0.1MB.
+            // bytes should always be greater than zero.
+            if (bytes <= 0) {
+                Log.e(LOGTAG, "sizeValueToString called with non-positive value: " + bytes);
+                return "0";
+            }
+            float megabytes = (float) bytes / (1024.0F * 1024.0F);
+            int truncated = (int) Math.ceil(megabytes * 10.0F);
+            float result = (float) (truncated / 10.0F);
+            return String.valueOf(result);
+        }
+
+        /*
+         * If we receive the back event and are displaying
+         * site's settings, we want to go back to the main
+         * list view. If not, we just do nothing (see
+         * dispatchKeyEvent() below).
+         */
+        public boolean backKeyPressed() {
+            if (mCurrentSite != null) {
+                mCurrentSite = null;
+                askForOrigins();
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * @hide
+         * Utility function
+         * Set the icon according to the usage
+         */
+        public void setIconForUsage(ImageView usageIcon, long usageInBytes) {
+            float usageInMegabytes = (float) usageInBytes / (1024.0F * 1024.0F);
+            // We set the correct icon:
+            // 0 < empty < 0.1MB
+            // 0.1MB < low < 5MB
+            // 5MB < high
+            if (usageInMegabytes <= 0.1) {
+                usageIcon.setImageBitmap(mUsageEmptyIcon);
+            } else if (usageInMegabytes > 0.1 && usageInMegabytes <= 5) {
+                usageIcon.setImageBitmap(mUsageLowIcon);
+            } else if (usageInMegabytes > 5) {
+                usageIcon.setImageBitmap(mUsageHighIcon);
+            }
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            View view;
+            final TextView title;
+            final TextView subtitle;
+            final ImageView icon;
+            final ImageView usageIcon;
+            final ImageView locationIcon;
+            final ImageView featureIcon;
+
+            if (convertView == null) {
+                view = mInflater.inflate(mResource, parent, false);
+            } else {
+                view = convertView;
+            }
+
+            title = (TextView) view.findViewById(R.id.title);
+            subtitle = (TextView) view.findViewById(R.id.subtitle);
+            icon = (ImageView) view.findViewById(R.id.icon);
+            featureIcon = (ImageView) view.findViewById(R.id.feature_icon);
+            usageIcon = (ImageView) view.findViewById(R.id.usage_icon);
+            locationIcon = (ImageView) view.findViewById(R.id.location_icon);
+            usageIcon.setVisibility(View.GONE);
+            locationIcon.setVisibility(View.GONE);
+
+            if (mCurrentSite == null) {
+
+                Site site = getItem(position);
+                title.setText(site.getPrettyTitle());
+                String subtitleText = site.getPrettyOrigin();
+                if (subtitleText != null) {
+                    title.setMaxLines(1);
+                    title.setSingleLine(true);
+                    subtitle.setVisibility(View.VISIBLE);
+                    subtitle.setText(subtitleText);
+                } else {
+                    subtitle.setVisibility(View.GONE);
+                    title.setMaxLines(2);
+                    title.setSingleLine(false);
+                }
+
+                icon.setVisibility(View.VISIBLE);
+                usageIcon.setVisibility(View.INVISIBLE);
+                locationIcon.setVisibility(View.INVISIBLE);
+                featureIcon.setVisibility(View.GONE);
+                Bitmap bmp = site.getIcon();
+                if (bmp == null) {
+                    bmp = mDefaultIcon;
+                }
+                icon.setImageBitmap(bmp);
+                // We set the site as the view's tag,
+                // so that we can get it in onItemClick()
+                view.setTag(site);
+
+                String origin = site.getOrigin();
+                if (site.hasFeature(Site.FEATURE_WEB_STORAGE)) {
+                    WebStorage.getInstance().getUsageForOrigin(origin, new ValueCallback<Long>() {
+                        public void onReceiveValue(Long value) {
+                            if (value != null) {
+                                setIconForUsage(usageIcon, value.longValue());
+                                usageIcon.setVisibility(View.VISIBLE);
+                            }
+                        }
+                    });
+                }
+
+                if (site.hasFeature(Site.FEATURE_GEOLOCATION)) {
+                    locationIcon.setVisibility(View.VISIBLE);
+                    GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
+                        public void onReceiveValue(Boolean allowed) {
+                            if (allowed != null) {
+                                if (allowed.booleanValue()) {
+                                    locationIcon.setImageBitmap(mLocationAllowedIcon);
+                                } else {
+                                    locationIcon.setImageBitmap(mLocationDisallowedIcon);
+                                }
+                            }
+                        }
+                    });
+                }
+            } else {
+                icon.setVisibility(View.GONE);
+                locationIcon.setVisibility(View.GONE);
+                usageIcon.setVisibility(View.GONE);
+                featureIcon.setVisibility(View.VISIBLE);
+                String origin = mCurrentSite.getOrigin();
+                switch (mCurrentSite.getFeatureByIndex(position)) {
+                    case Site.FEATURE_WEB_STORAGE:
+                        WebStorage.getInstance().getUsageForOrigin(origin, new ValueCallback<Long>() {
+                            public void onReceiveValue(Long value) {
+                                if (value != null) {
+                                    String usage = sizeValueToString(value.longValue()) + " " + sMBStored;
+                                    title.setText(R.string.webstorage_clear_data_title);
+                                    subtitle.setText(usage);
+                                    subtitle.setVisibility(View.VISIBLE);
+                                    setIconForUsage(featureIcon, value.longValue());
+                                }
+                            }
+                        });
+                        break;
+                    case Site.FEATURE_GEOLOCATION:
+                        title.setText(R.string.geolocation_settings_page_title);
+                        GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
+                            public void onReceiveValue(Boolean allowed) {
+                                if (allowed != null) {
+                                    if (allowed.booleanValue()) {
+                                        subtitle.setText(R.string.geolocation_settings_page_summary_allowed);
+                                        featureIcon.setImageBitmap(mLocationAllowedIcon);
+                                    } else {
+                                        subtitle.setText(R.string.geolocation_settings_page_summary_not_allowed);
+                                        featureIcon.setImageBitmap(mLocationDisallowedIcon);
+                                    }
+                                    subtitle.setVisibility(View.VISIBLE);
+                                }
+                            }
+                        });
+                        break;
+                }
+            }
+
+            return view;
+        }
+
+        public void onItemClick(AdapterView<?> parent,
+                                View view,
+                                int position,
+                                long id) {
+            if (mCurrentSite != null) {
+                switch (mCurrentSite.getFeatureByIndex(position)) {
+                    case Site.FEATURE_WEB_STORAGE:
+                        new AlertDialog.Builder(getContext())
+                            .setMessage(R.string.webstorage_clear_data_dialog_message)
+                            .setPositiveButton(R.string.webstorage_clear_data_dialog_ok_button,
+                                               new AlertDialog.OnClickListener() {
+                                public void onClick(DialogInterface dlg, int which) {
+                                    WebStorage.getInstance().deleteOrigin(mCurrentSite.getOrigin());
+                                    // If this site has no more features, then go back to the
+                                    // origins list.
+                                    mCurrentSite.removeFeature(Site.FEATURE_WEB_STORAGE);
+                                    if (mCurrentSite.getFeatureCount() == 0) {
+                                        finish();
+                                    }
+                                    askForOrigins();
+                                    notifyDataSetChanged();
+                                }})
+                            .setNegativeButton(R.string.webstorage_clear_data_dialog_cancel_button, null)
+                            .setIconAttribute(android.R.attr.alertDialogIcon)
+                            .show();
+                        break;
+                    case Site.FEATURE_GEOLOCATION:
+                        new AlertDialog.Builder(getContext())
+                            .setMessage(R.string.geolocation_settings_page_dialog_message)
+                            .setPositiveButton(R.string.geolocation_settings_page_dialog_ok_button,
+                                               new AlertDialog.OnClickListener() {
+                                public void onClick(DialogInterface dlg, int which) {
+                                    GeolocationPermissions.getInstance().clear(mCurrentSite.getOrigin());
+                                    mCurrentSite.removeFeature(Site.FEATURE_GEOLOCATION);
+                                    if (mCurrentSite.getFeatureCount() == 0) {
+                                        finish();
+                                    }
+                                    askForOrigins();
+                                    notifyDataSetChanged();
+                                }})
+                            .setNegativeButton(R.string.geolocation_settings_page_dialog_cancel_button, null)
+                            .setIconAttribute(android.R.attr.alertDialogIcon)
+                            .show();
+                        break;
+                }
+            } else {
+                Site site = (Site) view.getTag();
+                PreferenceActivity activity = (PreferenceActivity) getActivity();
+                if (activity != null) {
+                    Bundle args = new Bundle();
+                    args.putParcelable(EXTRA_SITE, site);
+                    activity.startPreferencePanel(WebsiteSettingsFragment.class.getName(), args, 0,
+                            site.getPrettyTitle(), null, 0);
+                }
+            }
+        }
+
+        public Site currentSite() {
+            return mCurrentSite;
+        }
+    }
+
+    @Override
+    public View onCreateView(LayoutInflater inflater, ViewGroup container,
+            Bundle savedInstanceState) {
+        View view = inflater.inflate(R.layout.website_settings, container, false);
+        Bundle args = getArguments();
+        if (args != null) {
+            mSite = (Site) args.getParcelable(EXTRA_SITE);
+        }
+        if (mSite == null) {
+            View clear = view.findViewById(R.id.clear_all_button);
+            clear.setVisibility(View.VISIBLE);
+            clear.setOnClickListener(this);
+        }
+        return view;
+    }
+
+    @Override
+    public void onActivityCreated(Bundle savedInstanceState) {
+        super.onActivityCreated(savedInstanceState);
+        if (sMBStored == null) {
+            sMBStored = getString(R.string.webstorage_origin_summary_mb_stored);
+        }
+        mAdapter = new SiteAdapter(getActivity(), R.layout.website_settings_row);
+        if (mSite != null) {
+            mAdapter.mCurrentSite = mSite;
+        }
+        getListView().setAdapter(mAdapter);
+        getListView().setOnItemClickListener(mAdapter);
+    }
+
+    @Override
+    public void onResume() {
+        super.onResume();
+        mAdapter.askForOrigins();
+    }
+
+    private void finish() {
+        PreferenceActivity activity = (PreferenceActivity) getActivity();
+        if (activity != null) {
+            activity.finishPreferencePanel(this, 0, null);
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        switch (v.getId()) {
+        case R.id.clear_all_button:
+         // Show the prompt to clear all origins of their data and geolocation permissions.
+            new AlertDialog.Builder(getActivity())
+                    .setMessage(R.string.website_settings_clear_all_dialog_message)
+                    .setPositiveButton(R.string.website_settings_clear_all_dialog_ok_button,
+                            new AlertDialog.OnClickListener() {
+                                public void onClick(DialogInterface dlg, int which) {
+                                    WebStorage.getInstance().deleteAllData();
+                                    GeolocationPermissions.getInstance().clearAll();
+                                    WebStorageSizeManager.resetLastOutOfSpaceNotificationTime();
+                                    mAdapter.askForOrigins();
+                                    finish();
+                                }})
+                    .setNegativeButton(R.string.website_settings_clear_all_dialog_cancel_button, null)
+                    .setIconAttribute(android.R.attr.alertDialogIcon)
+                    .show();
+            break;
+        }
+    }
+}
diff --git a/src/com/android/browser/provider/BrowserProvider.java b/src/com/android/browser/provider/BrowserProvider.java
new file mode 100644
index 0000000..744032c
--- /dev/null
+++ b/src/com/android/browser/provider/BrowserProvider.java
@@ -0,0 +1,1040 @@
+/*
+ * Copyright (C) 2006 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.provider;
+
+import android.app.SearchManager;
+import android.app.backup.BackupManager;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.UriMatcher;
+import android.content.res.Configuration;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+import android.os.Process;
+import android.preference.PreferenceManager;
+import android.provider.Browser;
+import android.provider.Browser.BookmarkColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Patterns;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.search.SearchEngine;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.Date;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class BrowserProvider extends ContentProvider {
+
+    private SQLiteOpenHelper mOpenHelper;
+    private BackupManager mBackupManager;
+    static final String sDatabaseName = "browser.db";
+    private static final String TAG = "BrowserProvider";
+    private static final String ORDER_BY = "visits DESC, date DESC";
+
+    private static final String PICASA_URL = "http://picasaweb.google.com/m/" +
+            "viewer?source=androidclient";
+
+    static final String[] TABLE_NAMES = new String[] {
+        "bookmarks", "searches"
+    };
+    private static final String[] SUGGEST_PROJECTION = new String[] {
+            "_id", "url", "title", "bookmark", "user_entered"
+    };
+    private static final String SUGGEST_SELECTION =
+            "(url LIKE ? OR url LIKE ? OR url LIKE ? OR url LIKE ?"
+                + " OR title LIKE ?) AND (bookmark = 1 OR user_entered = 1)";
+    private String[] SUGGEST_ARGS = new String[5];
+
+    // shared suggestion array index, make sure to match COLUMNS
+    private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
+    private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
+    private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
+    private static final int SUGGEST_COLUMN_TEXT_2_ID = 4;
+    private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
+    private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
+    private static final int SUGGEST_COLUMN_ICON_2_ID = 7;
+    private static final int SUGGEST_COLUMN_QUERY_ID = 8;
+    private static final int SUGGEST_COLUMN_INTENT_EXTRA_DATA = 9;
+
+    // how many suggestions will be shown in dropdown
+    // 0..SHORT: filled by browser db
+    private static final int MAX_SUGGEST_SHORT_SMALL = 3;
+    // SHORT..LONG: filled by search suggestions
+    private static final int MAX_SUGGEST_LONG_SMALL = 6;
+
+    // large screen size shows more
+    private static final int MAX_SUGGEST_SHORT_LARGE = 6;
+    private static final int MAX_SUGGEST_LONG_LARGE = 9;
+
+
+    // shared suggestion columns
+    private static final String[] COLUMNS = new String[] {
+            "_id",
+            SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+            SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+            SearchManager.SUGGEST_COLUMN_TEXT_1,
+            SearchManager.SUGGEST_COLUMN_TEXT_2,
+            SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
+            SearchManager.SUGGEST_COLUMN_ICON_1,
+            SearchManager.SUGGEST_COLUMN_ICON_2,
+            SearchManager.SUGGEST_COLUMN_QUERY,
+            SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA};
+
+
+    // make sure that these match the index of TABLE_NAMES
+    static final int URI_MATCH_BOOKMARKS = 0;
+    private static final int URI_MATCH_SEARCHES = 1;
+    // (id % 10) should match the table name index
+    private static final int URI_MATCH_BOOKMARKS_ID = 10;
+    private static final int URI_MATCH_SEARCHES_ID = 11;
+    //
+    private static final int URI_MATCH_SUGGEST = 20;
+    private static final int URI_MATCH_BOOKMARKS_SUGGEST = 21;
+
+    private static final UriMatcher URI_MATCHER;
+
+    static {
+        URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS],
+                URI_MATCH_BOOKMARKS);
+        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/#",
+                URI_MATCH_BOOKMARKS_ID);
+        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES],
+                URI_MATCH_SEARCHES);
+        URI_MATCHER.addURI("browser", TABLE_NAMES[URI_MATCH_SEARCHES] + "/#",
+                URI_MATCH_SEARCHES_ID);
+        URI_MATCHER.addURI("browser", SearchManager.SUGGEST_URI_PATH_QUERY,
+                URI_MATCH_SUGGEST);
+        URI_MATCHER.addURI("browser",
+                TABLE_NAMES[URI_MATCH_BOOKMARKS] + "/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+                URI_MATCH_BOOKMARKS_SUGGEST);
+    }
+
+    // 1 -> 2 add cache table
+    // 2 -> 3 update history table
+    // 3 -> 4 add passwords table
+    // 4 -> 5 add settings table
+    // 5 -> 6 ?
+    // 6 -> 7 ?
+    // 7 -> 8 drop proxy table
+    // 8 -> 9 drop settings table
+    // 9 -> 10 add form_urls and form_data
+    // 10 -> 11 add searches table
+    // 11 -> 12 modify cache table
+    // 12 -> 13 modify cache table
+    // 13 -> 14 correspond with Google Bookmarks schema
+    // 14 -> 15 move couple of tables to either browser private database or webview database
+    // 15 -> 17 Set it up for the SearchManager
+    // 17 -> 18 Added favicon in bookmarks table for Home shortcuts
+    // 18 -> 19 Remove labels table
+    // 19 -> 20 Added thumbnail
+    // 20 -> 21 Added touch_icon
+    // 21 -> 22 Remove "clientid"
+    // 22 -> 23 Added user_entered
+    // 23 -> 24 Url not allowed to be null anymore.
+    private static final int DATABASE_VERSION = 24;
+
+    // Regular expression which matches http://, followed by some stuff, followed by
+    // optionally a trailing slash, all matched as separate groups.
+    private static final Pattern STRIP_URL_PATTERN = Pattern.compile("^(http://)(.*?)(/$)?");
+
+    private BrowserSettings mSettings;
+
+    private int mMaxSuggestionShortSize;
+    private int mMaxSuggestionLongSize;
+
+    public BrowserProvider() {
+    }
+
+    // XXX: This is a major hack to remove our dependency on gsf constants and
+    // its content provider. http://b/issue?id=2425179
+    public static String getClientId(ContentResolver cr) {
+        String ret = "android-google";
+        Cursor legacyClientIdCursor = null;
+        Cursor searchClientIdCursor = null;
+
+        // search_client_id includes search prefix, legacy client_id does not include prefix
+        try {
+            searchClientIdCursor = cr.query(Uri.parse("content://com.google.settings/partner"),
+               new String[] { "value" }, "name='search_client_id'", null, null);
+            if (searchClientIdCursor != null && searchClientIdCursor.moveToNext()) {
+                ret = searchClientIdCursor.getString(0);
+            } else {
+                legacyClientIdCursor = cr.query(Uri.parse("content://com.google.settings/partner"),
+                    new String[] { "value" }, "name='client_id'", null, null);
+                if (legacyClientIdCursor != null && legacyClientIdCursor.moveToNext()) {
+                    ret = "ms-" + legacyClientIdCursor.getString(0);
+                }
+            }
+        } catch (RuntimeException ex) {
+            // fall through to return the default
+        } finally {
+            if (legacyClientIdCursor != null) {
+                legacyClientIdCursor.close();
+            }
+            if (searchClientIdCursor != null) {
+                searchClientIdCursor.close();
+            }
+        }
+        return ret;
+    }
+
+    private static CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString) {
+        StringBuffer sb = new StringBuffer();
+        int lastCharLoc = 0;
+
+        final String client_id = getClientId(context.getContentResolver());
+
+        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();
+                        if (propertyKeyValue.equals("CLIENT_ID")) {
+                            sb.append(client_id);
+                        } else {
+                            sb.append("unknown");
+                        }
+                        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;
+    }
+
+    static class DatabaseHelper extends SQLiteOpenHelper {
+        private Context mContext;
+
+        public DatabaseHelper(Context context) {
+            super(context, sDatabaseName, null, DATABASE_VERSION);
+            mContext = context;
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE bookmarks (" +
+                    "_id INTEGER PRIMARY KEY," +
+                    "title TEXT," +
+                    "url TEXT NOT NULL," +
+                    "visits INTEGER," +
+                    "date LONG," +
+                    "created LONG," +
+                    "description TEXT," +
+                    "bookmark INTEGER," +
+                    "favicon BLOB DEFAULT NULL," +
+                    "thumbnail BLOB DEFAULT NULL," +
+                    "touch_icon BLOB DEFAULT NULL," +
+                    "user_entered INTEGER" +
+                    ");");
+
+            final CharSequence[] bookmarks = mContext.getResources()
+                    .getTextArray(R.array.bookmarks);
+            int size = bookmarks.length;
+            try {
+                for (int i = 0; i < size; i = i + 2) {
+                    CharSequence bookmarkDestination = replaceSystemPropertyInString(mContext, bookmarks[i + 1]);
+                    db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
+                            "date, created, bookmark)" + " VALUES('" +
+                            bookmarks[i] + "', '" + bookmarkDestination +
+                            "', 0, 0, 0, 1);");
+                }
+            } catch (ArrayIndexOutOfBoundsException e) {
+            }
+
+            db.execSQL("CREATE TABLE searches (" +
+                    "_id INTEGER PRIMARY KEY," +
+                    "search TEXT," +
+                    "date LONG" +
+                    ");");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+                    + newVersion);
+            if (oldVersion == 18) {
+                db.execSQL("DROP TABLE IF EXISTS labels");
+            }
+            if (oldVersion <= 19) {
+                db.execSQL("ALTER TABLE bookmarks ADD COLUMN thumbnail BLOB DEFAULT NULL;");
+            }
+            if (oldVersion < 21) {
+                db.execSQL("ALTER TABLE bookmarks ADD COLUMN touch_icon BLOB DEFAULT NULL;");
+            }
+            if (oldVersion < 22) {
+                db.execSQL("DELETE FROM bookmarks WHERE (bookmark = 0 AND url LIKE \"%.google.%client=ms-%\")");
+                removeGears();
+            }
+            if (oldVersion < 23) {
+                db.execSQL("ALTER TABLE bookmarks ADD COLUMN user_entered INTEGER;");
+            }
+            if (oldVersion < 24) {
+                /* SQLite does not support ALTER COLUMN, hence the lengthy code. */
+                db.execSQL("DELETE FROM bookmarks WHERE url IS NULL;");
+                db.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_temp;");
+                db.execSQL("CREATE TABLE bookmarks (" +
+                        "_id INTEGER PRIMARY KEY," +
+                        "title TEXT," +
+                        "url TEXT NOT NULL," +
+                        "visits INTEGER," +
+                        "date LONG," +
+                        "created LONG," +
+                        "description TEXT," +
+                        "bookmark INTEGER," +
+                        "favicon BLOB DEFAULT NULL," +
+                        "thumbnail BLOB DEFAULT NULL," +
+                        "touch_icon BLOB DEFAULT NULL," +
+                        "user_entered INTEGER" +
+                        ");");
+                db.execSQL("INSERT INTO bookmarks SELECT * FROM bookmarks_temp;");
+                db.execSQL("DROP TABLE bookmarks_temp;");
+            } else {
+                db.execSQL("DROP TABLE IF EXISTS bookmarks");
+                db.execSQL("DROP TABLE IF EXISTS searches");
+                onCreate(db);
+            }
+        }
+
+        private void removeGears() {
+            new Thread() {
+                @Override
+                public void run() {
+                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+                    String browserDataDirString = mContext.getApplicationInfo().dataDir;
+                    final String appPluginsDirString = "app_plugins";
+                    final String gearsPrefix = "gears";
+                    File appPluginsDir = new File(browserDataDirString + File.separator
+                            + appPluginsDirString);
+                    if (!appPluginsDir.exists()) {
+                        return;
+                    }
+                    // Delete the Gears plugin files
+                    File[] gearsFiles = appPluginsDir.listFiles(new FilenameFilter() {
+                        public boolean accept(File dir, String filename) {
+                            return filename.startsWith(gearsPrefix);
+                        }
+                    });
+                    for (int i = 0; i < gearsFiles.length; ++i) {
+                        if (gearsFiles[i].isDirectory()) {
+                            deleteDirectory(gearsFiles[i]);
+                        } else {
+                            gearsFiles[i].delete();
+                        }
+                    }
+                    // Delete the Gears data files
+                    File gearsDataDir = new File(browserDataDirString + File.separator
+                            + gearsPrefix);
+                    if (!gearsDataDir.exists()) {
+                        return;
+                    }
+                    deleteDirectory(gearsDataDir);
+                }
+
+                private void deleteDirectory(File currentDir) {
+                    File[] files = currentDir.listFiles();
+                    for (int i = 0; i < files.length; ++i) {
+                        if (files[i].isDirectory()) {
+                            deleteDirectory(files[i]);
+                        }
+                        files[i].delete();
+                    }
+                    currentDir.delete();
+                }
+            }.start();
+        }
+    }
+
+    @Override
+    public boolean onCreate() {
+        final Context context = getContext();
+        boolean xlargeScreenSize = (context.getResources().getConfiguration().screenLayout
+                & Configuration.SCREENLAYOUT_SIZE_MASK)
+                == Configuration.SCREENLAYOUT_SIZE_XLARGE;
+        boolean isPortrait = (context.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_PORTRAIT);
+
+
+        if (xlargeScreenSize && isPortrait) {
+            mMaxSuggestionLongSize = MAX_SUGGEST_LONG_LARGE;
+            mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_LARGE;
+        } else {
+            mMaxSuggestionLongSize = MAX_SUGGEST_LONG_SMALL;
+            mMaxSuggestionShortSize = MAX_SUGGEST_SHORT_SMALL;
+        }
+        mOpenHelper = new DatabaseHelper(context);
+        mBackupManager = new BackupManager(context);
+        // we added "picasa web album" into default bookmarks for version 19.
+        // To avoid erasing the bookmark table, we added it explicitly for
+        // version 18 and 19 as in the other cases, we will erase the table.
+        if (DATABASE_VERSION == 18 || DATABASE_VERSION == 19) {
+            SharedPreferences p = PreferenceManager
+                    .getDefaultSharedPreferences(context);
+            boolean fix = p.getBoolean("fix_picasa", true);
+            if (fix) {
+                fixPicasaBookmark();
+                Editor ed = p.edit();
+                ed.putBoolean("fix_picasa", false);
+                ed.apply();
+            }
+        }
+        mSettings = BrowserSettings.getInstance();
+        return true;
+    }
+
+    private void fixPicasaBookmark() {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        Cursor cursor = db.rawQuery("SELECT _id FROM bookmarks WHERE " +
+                "bookmark = 1 AND url = ?", new String[] { PICASA_URL });
+        try {
+            if (!cursor.moveToFirst()) {
+                // set "created" so that it will be on the top of the list
+                db.execSQL("INSERT INTO bookmarks (title, url, visits, " +
+                        "date, created, bookmark)" + " VALUES('" +
+                        getContext().getString(R.string.picasa) + "', '"
+                        + PICASA_URL + "', 0, 0, " + new Date().getTime()
+                        + ", 1);");
+            }
+        } finally {
+            if (cursor != null) {
+                cursor.close();
+            }
+        }
+    }
+
+    /*
+     * Subclass AbstractCursor so we can combine multiple Cursors and add
+     * "Search the web".
+     * Here are the rules.
+     * 1. We only have MAX_SUGGESTION_LONG_ENTRIES in the list plus
+     *      "Search the web";
+     * 2. If bookmark/history entries has a match, "Search the web" shows up at
+     *      the second place. Otherwise, "Search the web" shows up at the first
+     *      place.
+     */
+    private class MySuggestionCursor extends AbstractCursor {
+        private Cursor  mHistoryCursor;
+        private Cursor  mSuggestCursor;
+        private int     mHistoryCount;
+        private int     mSuggestionCount;
+        private boolean mIncludeWebSearch;
+        private String  mString;
+        private int     mSuggestText1Id;
+        private int     mSuggestText2Id;
+        private int     mSuggestText2UrlId;
+        private int     mSuggestQueryId;
+        private int     mSuggestIntentExtraDataId;
+
+        public MySuggestionCursor(Cursor hc, Cursor sc, String string) {
+            mHistoryCursor = hc;
+            mSuggestCursor = sc;
+            mHistoryCount = hc != null ? hc.getCount() : 0;
+            mSuggestionCount = sc != null ? sc.getCount() : 0;
+            if (mSuggestionCount > (mMaxSuggestionLongSize - mHistoryCount)) {
+                mSuggestionCount = mMaxSuggestionLongSize - mHistoryCount;
+            }
+            mString = string;
+            mIncludeWebSearch = string.length() > 0;
+
+            // Some web suggest providers only give suggestions and have no description string for
+            // items. The order of the result columns may be different as well. So retrieve the
+            // column indices for the fields we need now and check before using below.
+            if (mSuggestCursor == null) {
+                mSuggestText1Id = -1;
+                mSuggestText2Id = -1;
+                mSuggestText2UrlId = -1;
+                mSuggestQueryId = -1;
+                mSuggestIntentExtraDataId = -1;
+            } else {
+                mSuggestText1Id = mSuggestCursor.getColumnIndex(
+                                SearchManager.SUGGEST_COLUMN_TEXT_1);
+                mSuggestText2Id = mSuggestCursor.getColumnIndex(
+                                SearchManager.SUGGEST_COLUMN_TEXT_2);
+                mSuggestText2UrlId = mSuggestCursor.getColumnIndex(
+                        SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
+                mSuggestQueryId = mSuggestCursor.getColumnIndex(
+                                SearchManager.SUGGEST_COLUMN_QUERY);
+                mSuggestIntentExtraDataId = mSuggestCursor.getColumnIndex(
+                                SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+            }
+        }
+
+        @Override
+        public boolean onMove(int oldPosition, int newPosition) {
+            if (mHistoryCursor == null) {
+                return false;
+            }
+            if (mIncludeWebSearch) {
+                if (mHistoryCount == 0 && newPosition == 0) {
+                    return true;
+                } else if (mHistoryCount > 0) {
+                    if (newPosition == 0) {
+                        mHistoryCursor.moveToPosition(0);
+                        return true;
+                    } else if (newPosition == 1) {
+                        return true;
+                    }
+                }
+                newPosition--;
+            }
+            if (mHistoryCount > newPosition) {
+                mHistoryCursor.moveToPosition(newPosition);
+            } else {
+                mSuggestCursor.moveToPosition(newPosition - mHistoryCount);
+            }
+            return true;
+        }
+
+        @Override
+        public int getCount() {
+            if (mIncludeWebSearch) {
+                return mHistoryCount + mSuggestionCount + 1;
+            } else {
+                return mHistoryCount + mSuggestionCount;
+            }
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            return COLUMNS;
+        }
+
+        @Override
+        public String getString(int columnIndex) {
+            if ((mPos != -1 && mHistoryCursor != null)) {
+                int type = -1; // 0: web search; 1: history; 2: suggestion
+                if (mIncludeWebSearch) {
+                    if (mHistoryCount == 0 && mPos == 0) {
+                        type = 0;
+                    } else if (mHistoryCount > 0) {
+                        if (mPos == 0) {
+                            type = 1;
+                        } else if (mPos == 1) {
+                            type = 0;
+                        }
+                    }
+                    if (type == -1) type = (mPos - 1) < mHistoryCount ? 1 : 2;
+                } else {
+                    type = mPos < mHistoryCount ? 1 : 2;
+                }
+
+                switch(columnIndex) {
+                    case SUGGEST_COLUMN_INTENT_ACTION_ID:
+                        if (type == 1) {
+                            return Intent.ACTION_VIEW;
+                        } else {
+                            return Intent.ACTION_SEARCH;
+                        }
+
+                    case SUGGEST_COLUMN_INTENT_DATA_ID:
+                        if (type == 1) {
+                            return mHistoryCursor.getString(1);
+                        } else {
+                            return null;
+                        }
+
+                    case SUGGEST_COLUMN_TEXT_1_ID:
+                        if (type == 0) {
+                            return mString;
+                        } else if (type == 1) {
+                            return getHistoryTitle();
+                        } else {
+                            if (mSuggestText1Id == -1) return null;
+                            return mSuggestCursor.getString(mSuggestText1Id);
+                        }
+
+                    case SUGGEST_COLUMN_TEXT_2_ID:
+                        if (type == 0) {
+                            return getContext().getString(R.string.search_the_web);
+                        } else if (type == 1) {
+                            return null;  // Use TEXT_2_URL instead
+                        } else {
+                            if (mSuggestText2Id == -1) return null;
+                            return mSuggestCursor.getString(mSuggestText2Id);
+                        }
+
+                    case SUGGEST_COLUMN_TEXT_2_URL_ID:
+                        if (type == 0) {
+                            return null;
+                        } else if (type == 1) {
+                            return getHistoryUrl();
+                        } else {
+                            if (mSuggestText2UrlId == -1) return null;
+                            return mSuggestCursor.getString(mSuggestText2UrlId);
+                        }
+
+                    case SUGGEST_COLUMN_ICON_1_ID:
+                        if (type == 1) {
+                            if (mHistoryCursor.getInt(3) == 1) {
+                                return Integer.valueOf(
+                                        R.drawable.ic_search_category_bookmark)
+                                        .toString();
+                            } else {
+                                return Integer.valueOf(
+                                        R.drawable.ic_search_category_history)
+                                        .toString();
+                            }
+                        } else {
+                            return Integer.valueOf(
+                                    R.drawable.ic_search_category_suggest)
+                                    .toString();
+                        }
+
+                    case SUGGEST_COLUMN_ICON_2_ID:
+                        return "0";
+
+                    case SUGGEST_COLUMN_QUERY_ID:
+                        if (type == 0) {
+                            return mString;
+                        } else if (type == 1) {
+                            // Return the url in the intent query column. This is ignored
+                            // within the browser because our searchable is set to
+                            // android:searchMode="queryRewriteFromData", but it is used by
+                            // global search for query rewriting.
+                            return mHistoryCursor.getString(1);
+                        } else {
+                            if (mSuggestQueryId == -1) return null;
+                            return mSuggestCursor.getString(mSuggestQueryId);
+                        }
+
+                    case SUGGEST_COLUMN_INTENT_EXTRA_DATA:
+                        if (type == 0) {
+                            return null;
+                        } else if (type == 1) {
+                            return null;
+                        } else {
+                            if (mSuggestIntentExtraDataId == -1) return null;
+                            return mSuggestCursor.getString(mSuggestIntentExtraDataId);
+                        }
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public double getDouble(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public float getFloat(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getInt(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getLong(int column) {
+            if ((mPos != -1) && column == 0) {
+                return mPos;        // use row# as the _Id
+            }
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public short getShort(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isNull(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        // TODO Temporary change, finalize after jq's changes go in
+        @Override
+        public void deactivate() {
+            if (mHistoryCursor != null) {
+                mHistoryCursor.deactivate();
+            }
+            if (mSuggestCursor != null) {
+                mSuggestCursor.deactivate();
+            }
+            super.deactivate();
+        }
+
+        @Override
+        public boolean requery() {
+            return (mHistoryCursor != null ? mHistoryCursor.requery() : false) |
+                    (mSuggestCursor != null ? mSuggestCursor.requery() : false);
+        }
+
+        // TODO Temporary change, finalize after jq's changes go in
+        @Override
+        public void close() {
+            super.close();
+            if (mHistoryCursor != null) {
+                mHistoryCursor.close();
+                mHistoryCursor = null;
+            }
+            if (mSuggestCursor != null) {
+                mSuggestCursor.close();
+                mSuggestCursor = null;
+            }
+        }
+
+        /**
+         * Provides the title (text line 1) for a browser suggestion, which should be the
+         * webpage title. If the webpage title is empty, returns the stripped url instead.
+         *
+         * @return the title string to use
+         */
+        private String getHistoryTitle() {
+            String title = mHistoryCursor.getString(2 /* webpage title */);
+            if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
+                title = stripUrl(mHistoryCursor.getString(1 /* url */));
+            }
+            return title;
+        }
+
+        /**
+         * Provides the subtitle (text line 2) for a browser suggestion, which should be the
+         * webpage url. If the webpage title is empty, then the url should go in the title
+         * instead, and the subtitle should be empty, so this would return null.
+         *
+         * @return the subtitle string to use, or null if none
+         */
+        private String getHistoryUrl() {
+            String title = mHistoryCursor.getString(2 /* webpage title */);
+            if (TextUtils.isEmpty(title) || TextUtils.getTrimmedLength(title) == 0) {
+                return null;
+            } else {
+                return stripUrl(mHistoryCursor.getString(1 /* url */));
+            }
+        }
+
+    }
+
+    @Override
+    public Cursor query(Uri url, String[] projectionIn, String selection,
+            String[] selectionArgs, String sortOrder)
+            throws IllegalStateException {
+        int match = URI_MATCHER.match(url);
+        if (match == -1) {
+            throw new IllegalArgumentException("Unknown URL");
+        }
+
+        if (match == URI_MATCH_SUGGEST || match == URI_MATCH_BOOKMARKS_SUGGEST) {
+            // Handle suggestions
+            return doSuggestQuery(selection, selectionArgs, match == URI_MATCH_BOOKMARKS_SUGGEST);
+        }
+
+        String[] projection = null;
+        if (projectionIn != null && projectionIn.length > 0) {
+            projection = new String[projectionIn.length + 1];
+            System.arraycopy(projectionIn, 0, projection, 0, projectionIn.length);
+            projection[projectionIn.length] = "_id AS _id";
+        }
+
+        String whereClause = null;
+        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+            whereClause = "_id = " + url.getPathSegments().get(1);
+        }
+
+        Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[match % 10], projection,
+                DatabaseUtils.concatenateWhere(whereClause, selection), selectionArgs,
+                null, null, sortOrder, null);
+        c.setNotificationUri(getContext().getContentResolver(), url);
+        return c;
+    }
+
+    private Cursor doSuggestQuery(String selection, String[] selectionArgs, boolean bookmarksOnly) {
+        String suggestSelection;
+        String [] myArgs;
+        if (selectionArgs[0] == null || selectionArgs[0].equals("")) {
+            return new MySuggestionCursor(null, null, "");
+        } else {
+            String like = selectionArgs[0] + "%";
+            if (selectionArgs[0].startsWith("http")
+                    || selectionArgs[0].startsWith("file")) {
+                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;
+                // To match against titles.
+                SUGGEST_ARGS[4] = like;
+                myArgs = SUGGEST_ARGS;
+                suggestSelection = SUGGEST_SELECTION;
+            }
+        }
+
+        Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_NAMES[URI_MATCH_BOOKMARKS],
+                SUGGEST_PROJECTION, suggestSelection, myArgs, null, null,
+                ORDER_BY, Integer.toString(mMaxSuggestionLongSize));
+
+        if (bookmarksOnly || Patterns.WEB_URL.matcher(selectionArgs[0]).matches()) {
+            return new MySuggestionCursor(c, null, "");
+        } else {
+            // get search suggestions if there is still space in the list
+            if (myArgs != null && myArgs.length > 1
+                    && c.getCount() < (MAX_SUGGEST_SHORT_SMALL - 1)) {
+                SearchEngine searchEngine = mSettings.getSearchEngine();
+                if (searchEngine != null && searchEngine.supportsSuggestions()) {
+                    Cursor sc = searchEngine.getSuggestions(getContext(), selectionArgs[0]);
+                    return new MySuggestionCursor(c, sc, selectionArgs[0]);
+                }
+            }
+            return new MySuggestionCursor(c, null, selectionArgs[0]);
+        }
+    }
+
+    @Override
+    public String getType(Uri url) {
+        int match = URI_MATCHER.match(url);
+        switch (match) {
+            case URI_MATCH_BOOKMARKS:
+                return "vnd.android.cursor.dir/bookmark";
+
+            case URI_MATCH_BOOKMARKS_ID:
+                return "vnd.android.cursor.item/bookmark";
+
+            case URI_MATCH_SEARCHES:
+                return "vnd.android.cursor.dir/searches";
+
+            case URI_MATCH_SEARCHES_ID:
+                return "vnd.android.cursor.item/searches";
+
+            case URI_MATCH_SUGGEST:
+                return SearchManager.SUGGEST_MIME_TYPE;
+
+            default:
+                throw new IllegalArgumentException("Unknown URL");
+        }
+    }
+
+    @Override
+    public Uri insert(Uri url, ContentValues initialValues) {
+        boolean isBookmarkTable = false;
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+        int match = URI_MATCHER.match(url);
+        Uri uri = null;
+        switch (match) {
+            case URI_MATCH_BOOKMARKS: {
+                // Insert into the bookmarks table
+                long rowID = db.insert(TABLE_NAMES[URI_MATCH_BOOKMARKS], "url",
+                        initialValues);
+                if (rowID > 0) {
+                    uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
+                            rowID);
+                }
+                isBookmarkTable = true;
+                break;
+            }
+
+            case URI_MATCH_SEARCHES: {
+                // Insert into the searches table
+                long rowID = db.insert(TABLE_NAMES[URI_MATCH_SEARCHES], "url",
+                        initialValues);
+                if (rowID > 0) {
+                    uri = ContentUris.withAppendedId(Browser.SEARCHES_URI,
+                            rowID);
+                }
+                break;
+            }
+
+            default:
+                throw new IllegalArgumentException("Unknown URL");
+        }
+
+        if (uri == null) {
+            throw new IllegalArgumentException("Unknown URL");
+        }
+        getContext().getContentResolver().notifyChange(uri, null);
+
+        // Back up the new bookmark set if we just inserted one.
+        // A row created when bookmarks are added from scratch will have
+        // bookmark=1 in the initial value set.
+        if (isBookmarkTable
+                && initialValues.containsKey(BookmarkColumns.BOOKMARK)
+                && initialValues.getAsInteger(BookmarkColumns.BOOKMARK) != 0) {
+            mBackupManager.dataChanged();
+        }
+        return uri;
+    }
+
+    @Override
+    public int delete(Uri url, String where, String[] whereArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+        int match = URI_MATCHER.match(url);
+        if (match == -1 || match == URI_MATCH_SUGGEST) {
+            throw new IllegalArgumentException("Unknown URL");
+        }
+
+        // need to know whether it's the bookmarks table for a couple of reasons
+        boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
+        String id = null;
+
+        if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
+            StringBuilder sb = new StringBuilder();
+            if (where != null && where.length() > 0) {
+                sb.append("( ");
+                sb.append(where);
+                sb.append(" ) AND ");
+            }
+            id = url.getPathSegments().get(1);
+            sb.append("_id = ");
+            sb.append(id);
+            where = sb.toString();
+        }
+
+        ContentResolver cr = getContext().getContentResolver();
+
+        // we'lll need to back up the bookmark set if we are about to delete one
+        if (isBookmarkTable) {
+            Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+                    new String[] { BookmarkColumns.BOOKMARK },
+                    "_id = " + id, null, null);
+            if (cursor.moveToNext()) {
+                if (cursor.getInt(0) != 0) {
+                    // yep, this record is a bookmark
+                    mBackupManager.dataChanged();
+                }
+            }
+            cursor.close();
+        }
+
+        int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
+        cr.notifyChange(url, null);
+        return count;
+    }
+
+    @Override
+    public int update(Uri url, ContentValues values, String where,
+            String[] whereArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+
+        int match = URI_MATCHER.match(url);
+        if (match == -1 || match == URI_MATCH_SUGGEST) {
+            throw new IllegalArgumentException("Unknown URL");
+        }
+
+        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+            StringBuilder sb = new StringBuilder();
+            if (where != null && where.length() > 0) {
+                sb.append("( ");
+                sb.append(where);
+                sb.append(" ) AND ");
+            }
+            String id = url.getPathSegments().get(1);
+            sb.append("_id = ");
+            sb.append(id);
+            where = sb.toString();
+        }
+
+        ContentResolver cr = getContext().getContentResolver();
+
+        // Not all bookmark-table updates should be backed up.  Look to see
+        // whether we changed the title, url, or "is a bookmark" state, and
+        // request a backup if so.
+        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_BOOKMARKS) {
+            boolean changingBookmarks = false;
+            // Alterations to the bookmark field inherently change the bookmark
+            // set, so we don't need to query the record; we know a priori that
+            // we will need to back up this change.
+            if (values.containsKey(BookmarkColumns.BOOKMARK)) {
+                changingBookmarks = true;
+            } else if ((values.containsKey(BookmarkColumns.TITLE)
+                     || values.containsKey(BookmarkColumns.URL))
+                     && values.containsKey(BookmarkColumns._ID)) {
+                // If a title or URL has been changed, check to see if it is to
+                // a bookmark.  The ID should have been included in the update,
+                // so use it.
+                Cursor cursor = cr.query(Browser.BOOKMARKS_URI,
+                        new String[] { BookmarkColumns.BOOKMARK },
+                        BookmarkColumns._ID + " = "
+                        + values.getAsString(BookmarkColumns._ID), null, null);
+                if (cursor.moveToNext()) {
+                    changingBookmarks = (cursor.getInt(0) != 0);
+                }
+                cursor.close();
+            }
+
+            // if this *is* a bookmark row we're altering, we need to back it up.
+            if (changingBookmarks) {
+                mBackupManager.dataChanged();
+            }
+        }
+
+        int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
+        cr.notifyChange(url, null);
+        return ret;
+    }
+
+    /**
+     * Strips the provided url of preceding "http://" and any trailing "/". Does not
+     * strip "https://". If the provided string cannot be stripped, the original string
+     * is returned.
+     *
+     * TODO: Put this in TextUtils to be used by other packages doing something similar.
+     *
+     * @param url a url to strip, like "http://www.google.com/"
+     * @return a stripped url like "www.google.com", or the original string if it could
+     *         not be stripped
+     */
+    private static String stripUrl(String url) {
+        if (url == null) return null;
+        Matcher m = STRIP_URL_PATTERN.matcher(url);
+        if (m.matches() && m.groupCount() == 3) {
+            return m.group(2);
+        } else {
+            return url;
+        }
+    }
+
+    public static Cursor getBookmarksSuggestions(ContentResolver cr, String constraint) {
+        Uri uri = Uri.parse("content://browser/" + SearchManager.SUGGEST_URI_PATH_QUERY);
+        return cr.query(uri, SUGGEST_PROJECTION, SUGGEST_SELECTION,
+            new String[] { constraint }, ORDER_BY);
+    }
+
+}
diff --git a/src/com/android/browser/provider/BrowserProvider2.java b/src/com/android/browser/provider/BrowserProvider2.java
new file mode 100644
index 0000000..cbb39b6
--- /dev/null
+++ b/src/com/android/browser/provider/BrowserProvider2.java
@@ -0,0 +1,2366 @@
+/*
+ * Copyright (C) 2010 he 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.provider;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.SearchManager;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UriMatcher;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.AbstractCursor;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.MatrixCursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+import android.provider.BaseColumns;
+import android.provider.Browser;
+import android.provider.ContactsContract.RawContacts;
+import android.provider.SyncStateContract;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.UrlUtils;
+import com.android.browser.platformsupport.BookmarkColumns;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.SyncStateContentProviderHelper;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.platformsupport.BrowserContract.ChromeSyncColumns;
+import com.android.browser.platformsupport.BrowserContract.Combined;
+import com.android.browser.platformsupport.BrowserContract.History;
+import com.android.browser.platformsupport.BrowserContract.Images;
+import com.android.browser.platformsupport.BrowserContract.Searches;
+import com.android.browser.platformsupport.BrowserContract.Settings;
+import com.android.browser.platformsupport.BrowserContract.SyncState;
+import com.android.browser.reflect.ReflectHelper;
+import com.android.browser.widget.BookmarkThumbnailWidgetProvider;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class BrowserProvider2 extends SQLiteContentProvider {
+
+    private static final String TAG = "BrowserProvider2";
+
+    public static final String PARAM_GROUP_BY = "groupBy";
+    public static final String PARAM_ALLOW_EMPTY_ACCOUNTS = "allowEmptyAccounts";
+
+    public static final String LEGACY_AUTHORITY = "browser";
+    static final Uri LEGACY_AUTHORITY_URI = new Uri.Builder()
+            .authority(LEGACY_AUTHORITY).scheme("content").build();
+
+    public static interface Thumbnails {
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(
+                BrowserContract.AUTHORITY_URI, "thumbnails");
+        public static final String _ID = "_id";
+        public static final String THUMBNAIL = "thumbnail";
+    }
+
+    public static interface OmniboxSuggestions {
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(
+                BrowserContract.AUTHORITY_URI, "omnibox_suggestions");
+        public static final String _ID = "_id";
+        public static final String URL = "url";
+        public static final String TITLE = "title";
+        public static final String IS_BOOKMARK = "bookmark";
+    }
+
+    static final String TABLE_BOOKMARKS = "bookmarks";
+    static final String TABLE_HISTORY = "history";
+    static final String TABLE_IMAGES = "images";
+    static final String TABLE_SEARCHES = "searches";
+    static final String TABLE_SYNC_STATE = "syncstate";
+    static final String TABLE_SETTINGS = "settings";
+    static final String TABLE_SNAPSHOTS = "snapshots";
+    static final String TABLE_THUMBNAILS = "thumbnails";
+
+    static final String TABLE_BOOKMARKS_JOIN_IMAGES = "bookmarks LEFT OUTER JOIN images " +
+            "ON bookmarks.url = images." + Images.URL;
+    static final String TABLE_HISTORY_JOIN_IMAGES = "history LEFT OUTER JOIN images " +
+            "ON history.url = images." + Images.URL;
+
+    static final String VIEW_ACCOUNTS = "v_accounts";
+    static final String VIEW_SNAPSHOTS_COMBINED = "v_snapshots_combined";
+    static final String VIEW_OMNIBOX_SUGGESTIONS = "v_omnibox_suggestions";
+
+    static final String FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES =
+            "history LEFT OUTER JOIN (%s) bookmarks " +
+            "ON history.url = bookmarks.url LEFT OUTER JOIN images " +
+            "ON history.url = images.url_key";
+
+    static final String DEFAULT_SORT_HISTORY = History.DATE_LAST_VISITED + " DESC";
+    static final String DEFAULT_SORT_ACCOUNTS =
+            Accounts.ACCOUNT_NAME + " IS NOT NULL DESC, "
+            + Accounts.ACCOUNT_NAME + " ASC";
+
+    private static final String TABLE_BOOKMARKS_JOIN_HISTORY =
+        "history LEFT OUTER JOIN bookmarks ON history.url = bookmarks.url";
+
+    private static final String[] SUGGEST_PROJECTION = new String[] {
+            qualifyColumn(TABLE_HISTORY, History._ID),
+            qualifyColumn(TABLE_HISTORY, History.URL),
+            bookmarkOrHistoryColumn(Combined.TITLE),
+            bookmarkOrHistoryLiteral(Combined.URL,
+                    Integer.toString(R.drawable.ic_bookmark_off_holo_dark),
+                    Integer.toString(R.drawable.ic_history_holo_dark)),
+            qualifyColumn(TABLE_HISTORY, History.DATE_LAST_VISITED)};
+
+    private static final String SUGGEST_SELECTION =
+            "history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ? OR history.url LIKE ?"
+            + " OR history.title LIKE ? OR bookmarks.title LIKE ?";
+
+    private static final String ZERO_QUERY_SUGGEST_SELECTION =
+            TABLE_HISTORY + "." + History.DATE_LAST_VISITED + " != 0";
+
+    private static final String IMAGE_PRUNE =
+            "url_key NOT IN (SELECT url FROM bookmarks " +
+            "WHERE url IS NOT NULL AND deleted == 0) AND url_key NOT IN " +
+            "(SELECT url FROM history WHERE url IS NOT NULL)";
+
+    static final int THUMBNAILS = 10;
+    static final int THUMBNAILS_ID = 11;
+    static final int OMNIBOX_SUGGESTIONS = 20;
+    static final int HOMEPAGE = 60;
+
+    static final int BOOKMARKS = 1000;
+    static final int BOOKMARKS_ID = 1001;
+    static final int BOOKMARKS_FOLDER = 1002;
+    static final int BOOKMARKS_FOLDER_ID = 1003;
+    static final int BOOKMARKS_SUGGESTIONS = 1004;
+    static final int BOOKMARKS_DEFAULT_FOLDER_ID = 1005;
+
+    static final int HISTORY = 2000;
+    static final int HISTORY_ID = 2001;
+
+    static final int SEARCHES = 3000;
+    static final int SEARCHES_ID = 3001;
+
+    static final int SYNCSTATE = 4000;
+    static final int SYNCSTATE_ID = 4001;
+
+    static final int IMAGES = 5000;
+
+    static final int COMBINED = 6000;
+    static final int COMBINED_ID = 6001;
+
+    static final int ACCOUNTS = 7000;
+
+    static final int SETTINGS = 8000;
+
+    static final int LEGACY = 9000;
+    static final int LEGACY_ID = 9001;
+
+    public static final long FIXED_ID_ROOT = 1;
+
+    // Default sort order for unsync'd bookmarks
+    static final String DEFAULT_BOOKMARKS_SORT_ORDER =
+            Bookmarks.IS_FOLDER + " DESC, position ASC, _id ASC";
+
+    // Default sort order for sync'd bookmarks
+    static final String DEFAULT_BOOKMARKS_SORT_ORDER_SYNC = "position ASC, _id ASC";
+
+    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+
+    static final HashMap<String, String> ACCOUNTS_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> BOOKMARKS_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> OTHER_BOOKMARKS_PROJECTION_MAP =
+            new HashMap<String, String>();
+    static final HashMap<String, String> HISTORY_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> SYNC_STATE_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> IMAGES_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> COMBINED_HISTORY_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> COMBINED_BOOKMARK_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> SEARCHES_PROJECTION_MAP = new HashMap<String, String>();
+    static final HashMap<String, String> SETTINGS_PROJECTION_MAP = new HashMap<String, String>();
+
+    static {
+        final UriMatcher matcher = URI_MATCHER;
+        final String authority = BrowserContract.AUTHORITY;
+        matcher.addURI(authority, "accounts", ACCOUNTS);
+        matcher.addURI(authority, "bookmarks", BOOKMARKS);
+        matcher.addURI(authority, "bookmarks/#", BOOKMARKS_ID);
+        matcher.addURI(authority, "bookmarks/folder", BOOKMARKS_FOLDER);
+        matcher.addURI(authority, "bookmarks/folder/#", BOOKMARKS_FOLDER_ID);
+        matcher.addURI(authority, "bookmarks/folder/id", BOOKMARKS_DEFAULT_FOLDER_ID);
+        matcher.addURI(authority,
+                SearchManager.SUGGEST_URI_PATH_QUERY,
+                BOOKMARKS_SUGGESTIONS);
+        matcher.addURI(authority,
+                "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+                BOOKMARKS_SUGGESTIONS);
+        matcher.addURI(authority, "history", HISTORY);
+        matcher.addURI(authority, "history/#", HISTORY_ID);
+        matcher.addURI(authority, "searches", SEARCHES);
+        matcher.addURI(authority, "searches/#", SEARCHES_ID);
+        matcher.addURI(authority, "syncstate", SYNCSTATE);
+        matcher.addURI(authority, "syncstate/#", SYNCSTATE_ID);
+        matcher.addURI(authority, "images", IMAGES);
+        matcher.addURI(authority, "combined", COMBINED);
+        matcher.addURI(authority, "combined/#", COMBINED_ID);
+        matcher.addURI(authority, "settings", SETTINGS);
+        matcher.addURI(authority, "thumbnails", THUMBNAILS);
+        matcher.addURI(authority, "thumbnails/#", THUMBNAILS_ID);
+        matcher.addURI(authority, "omnibox_suggestions", OMNIBOX_SUGGESTIONS);
+        matcher.addURI(authority, "homepage", HOMEPAGE);
+
+        // Legacy
+        matcher.addURI(LEGACY_AUTHORITY, "searches", SEARCHES);
+        matcher.addURI(LEGACY_AUTHORITY, "searches/#", SEARCHES_ID);
+        matcher.addURI(LEGACY_AUTHORITY, "bookmarks", LEGACY);
+        matcher.addURI(LEGACY_AUTHORITY, "bookmarks/#", LEGACY_ID);
+        matcher.addURI(LEGACY_AUTHORITY,
+                SearchManager.SUGGEST_URI_PATH_QUERY,
+                BOOKMARKS_SUGGESTIONS);
+        matcher.addURI(LEGACY_AUTHORITY,
+                "bookmarks/" + SearchManager.SUGGEST_URI_PATH_QUERY,
+                BOOKMARKS_SUGGESTIONS);
+
+        // Projection maps
+        HashMap<String, String> map;
+
+        // Accounts
+        map = ACCOUNTS_PROJECTION_MAP;
+        map.put(Accounts.ACCOUNT_TYPE, Accounts.ACCOUNT_TYPE);
+        map.put(Accounts.ACCOUNT_NAME, Accounts.ACCOUNT_NAME);
+        map.put(Accounts.ROOT_ID, Accounts.ROOT_ID);
+
+        // Bookmarks
+        map = BOOKMARKS_PROJECTION_MAP;
+        map.put(Bookmarks._ID, qualifyColumn(TABLE_BOOKMARKS, Bookmarks._ID));
+        map.put(Bookmarks.TITLE, Bookmarks.TITLE);
+        map.put(Bookmarks.URL, Bookmarks.URL);
+        map.put(Bookmarks.FAVICON, Bookmarks.FAVICON);
+        map.put(Bookmarks.THUMBNAIL, Bookmarks.THUMBNAIL);
+        map.put(Bookmarks.TOUCH_ICON, Bookmarks.TOUCH_ICON);
+        map.put(Bookmarks.IS_FOLDER, Bookmarks.IS_FOLDER);
+        map.put(Bookmarks.PARENT, Bookmarks.PARENT);
+        map.put(Bookmarks.POSITION, Bookmarks.POSITION);
+        map.put(Bookmarks.INSERT_AFTER, Bookmarks.INSERT_AFTER);
+        map.put(Bookmarks.IS_DELETED, Bookmarks.IS_DELETED);
+        map.put(Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_NAME);
+        map.put(Bookmarks.ACCOUNT_TYPE, Bookmarks.ACCOUNT_TYPE);
+        map.put(Bookmarks.SOURCE_ID, Bookmarks.SOURCE_ID);
+        map.put(Bookmarks.VERSION, Bookmarks.VERSION);
+        map.put(Bookmarks.DATE_CREATED, Bookmarks.DATE_CREATED);
+        map.put(Bookmarks.DATE_MODIFIED, Bookmarks.DATE_MODIFIED);
+        map.put(Bookmarks.DIRTY, Bookmarks.DIRTY);
+        map.put(Bookmarks.SYNC1, Bookmarks.SYNC1);
+        map.put(Bookmarks.SYNC2, Bookmarks.SYNC2);
+        map.put(Bookmarks.SYNC3, Bookmarks.SYNC3);
+        map.put(Bookmarks.SYNC4, Bookmarks.SYNC4);
+        map.put(Bookmarks.SYNC5, Bookmarks.SYNC5);
+        map.put(Bookmarks.PARENT_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID +
+                " FROM " + TABLE_BOOKMARKS + " A WHERE " +
+                "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.PARENT +
+                ") AS " + Bookmarks.PARENT_SOURCE_ID);
+        map.put(Bookmarks.INSERT_AFTER_SOURCE_ID, "(SELECT " + Bookmarks.SOURCE_ID +
+                " FROM " + TABLE_BOOKMARKS + " A WHERE " +
+                "A." + Bookmarks._ID + "=" + TABLE_BOOKMARKS + "." + Bookmarks.INSERT_AFTER +
+                ") AS " + Bookmarks.INSERT_AFTER_SOURCE_ID);
+        map.put(Bookmarks.TYPE, "CASE "
+                + " WHEN " + Bookmarks.IS_FOLDER + "=0 THEN "
+                    + Bookmarks.BOOKMARK_TYPE_BOOKMARK
+                + " WHEN " + ChromeSyncColumns.SERVER_UNIQUE + "='"
+                    + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' THEN "
+                    + Bookmarks.BOOKMARK_TYPE_BOOKMARK_BAR_FOLDER
+                + " WHEN " + ChromeSyncColumns.SERVER_UNIQUE + "='"
+                    + ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS + "' THEN "
+                    + Bookmarks.BOOKMARK_TYPE_OTHER_FOLDER
+                + " ELSE " + Bookmarks.BOOKMARK_TYPE_FOLDER
+                + " END AS " + Bookmarks.TYPE);
+
+        // Other bookmarks
+        OTHER_BOOKMARKS_PROJECTION_MAP.putAll(BOOKMARKS_PROJECTION_MAP);
+        OTHER_BOOKMARKS_PROJECTION_MAP.put(Bookmarks.POSITION,
+                Long.toString(Long.MAX_VALUE) + " AS " + Bookmarks.POSITION);
+
+        // History
+        map = HISTORY_PROJECTION_MAP;
+        map.put(History._ID, qualifyColumn(TABLE_HISTORY, History._ID));
+        map.put(History.TITLE, History.TITLE);
+        map.put(History.URL, History.URL);
+        map.put(History.FAVICON, History.FAVICON);
+        map.put(History.THUMBNAIL, History.THUMBNAIL);
+        map.put(History.TOUCH_ICON, History.TOUCH_ICON);
+        map.put(History.DATE_CREATED, History.DATE_CREATED);
+        map.put(History.DATE_LAST_VISITED, History.DATE_LAST_VISITED);
+        map.put(History.VISITS, History.VISITS);
+        map.put(History.USER_ENTERED, History.USER_ENTERED);
+
+        // Sync state
+        map = SYNC_STATE_PROJECTION_MAP;
+        map.put(SyncState._ID, SyncState._ID);
+        map.put(SyncState.ACCOUNT_NAME, SyncState.ACCOUNT_NAME);
+        map.put(SyncState.ACCOUNT_TYPE, SyncState.ACCOUNT_TYPE);
+        map.put(SyncState.DATA, SyncState.DATA);
+
+        // Images
+        map = IMAGES_PROJECTION_MAP;
+        map.put(Images.URL, Images.URL);
+        map.put(Images.FAVICON, Images.FAVICON);
+        map.put(Images.THUMBNAIL, Images.THUMBNAIL);
+        map.put(Images.TOUCH_ICON, Images.TOUCH_ICON);
+
+        // Combined history half
+        map = COMBINED_HISTORY_PROJECTION_MAP;
+        map.put(Combined._ID, bookmarkOrHistoryColumn(Combined._ID));
+        map.put(Combined.TITLE, bookmarkOrHistoryColumn(Combined.TITLE));
+        map.put(Combined.URL, qualifyColumn(TABLE_HISTORY, Combined.URL));
+        map.put(Combined.DATE_CREATED, qualifyColumn(TABLE_HISTORY, Combined.DATE_CREATED));
+        map.put(Combined.DATE_LAST_VISITED, Combined.DATE_LAST_VISITED);
+        map.put(Combined.IS_BOOKMARK, "CASE WHEN " +
+                TABLE_BOOKMARKS + "." + Bookmarks._ID +
+                " IS NOT NULL THEN 1 ELSE 0 END AS " + Combined.IS_BOOKMARK);
+        map.put(Combined.VISITS, Combined.VISITS);
+        map.put(Combined.FAVICON, Combined.FAVICON);
+        map.put(Combined.THUMBNAIL, Combined.THUMBNAIL);
+        map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON);
+        map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED);
+
+        // Combined bookmark half
+        map = COMBINED_BOOKMARK_PROJECTION_MAP;
+        map.put(Combined._ID, Combined._ID);
+        map.put(Combined.TITLE, Combined.TITLE);
+        map.put(Combined.URL, Combined.URL);
+        map.put(Combined.DATE_CREATED, Combined.DATE_CREATED);
+        map.put(Combined.DATE_LAST_VISITED, "NULL AS " + Combined.DATE_LAST_VISITED);
+        map.put(Combined.IS_BOOKMARK, "1 AS " + Combined.IS_BOOKMARK);
+        map.put(Combined.VISITS, "0 AS " + Combined.VISITS);
+        map.put(Combined.FAVICON, Combined.FAVICON);
+        map.put(Combined.THUMBNAIL, Combined.THUMBNAIL);
+        map.put(Combined.TOUCH_ICON, Combined.TOUCH_ICON);
+        map.put(Combined.USER_ENTERED, "NULL AS " + Combined.USER_ENTERED);
+
+        // Searches
+        map = SEARCHES_PROJECTION_MAP;
+        map.put(Searches._ID, Searches._ID);
+        map.put(Searches.SEARCH, Searches.SEARCH);
+        map.put(Searches.DATE, Searches.DATE);
+
+        // Settings
+        map = SETTINGS_PROJECTION_MAP;
+        map.put(Settings.KEY, Settings.KEY);
+        map.put(Settings.VALUE, Settings.VALUE);
+    }
+
+    static final String bookmarkOrHistoryColumn(String column) {
+        return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN " +
+                "bookmarks." + column + " ELSE history." + column + " END AS " + column;
+    }
+
+    static final String bookmarkOrHistoryLiteral(String column, String bookmarkValue,
+            String historyValue) {
+        return "CASE WHEN bookmarks." + column + " IS NOT NULL THEN \"" + bookmarkValue +
+                "\" ELSE \"" + historyValue + "\" END";
+    }
+
+    static final String qualifyColumn(String table, String column) {
+        return table + "." + column + " AS " + column;
+    }
+
+    DatabaseHelper mOpenHelper;
+    SyncStateContentProviderHelper mSyncHelper = new SyncStateContentProviderHelper();
+    // This is so provider tests can intercept widget updating
+    ContentObserver mWidgetObserver = null;
+    boolean mUpdateWidgets = false;
+    boolean mSyncToNetwork = true;
+
+    final class DatabaseHelper extends SQLiteOpenHelper {
+        static final String DATABASE_NAME = "browser2.db";
+        static final int DATABASE_VERSION = 32;
+        public DatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+            setWriteAheadLoggingEnabled(true);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_BOOKMARKS + "(" +
+                    Bookmarks._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    Bookmarks.TITLE + " TEXT," +
+                    Bookmarks.URL + " TEXT," +
+                    Bookmarks.IS_FOLDER + " INTEGER NOT NULL DEFAULT 0," +
+                    Bookmarks.PARENT + " INTEGER," +
+                    Bookmarks.POSITION + " INTEGER NOT NULL," +
+                    Bookmarks.INSERT_AFTER + " INTEGER," +
+                    Bookmarks.IS_DELETED + " INTEGER NOT NULL DEFAULT 0," +
+                    Bookmarks.ACCOUNT_NAME + " TEXT," +
+                    Bookmarks.ACCOUNT_TYPE + " TEXT," +
+                    Bookmarks.SOURCE_ID + " TEXT," +
+                    Bookmarks.VERSION + " INTEGER NOT NULL DEFAULT 1," +
+                    Bookmarks.DATE_CREATED + " INTEGER," +
+                    Bookmarks.DATE_MODIFIED + " INTEGER," +
+                    Bookmarks.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
+                    Bookmarks.SYNC1 + " TEXT," +
+                    Bookmarks.SYNC2 + " TEXT," +
+                    Bookmarks.SYNC3 + " TEXT," +
+                    Bookmarks.SYNC4 + " TEXT," +
+                    Bookmarks.SYNC5 + " TEXT" +
+                    ");");
+
+            // TODO indices
+
+            db.execSQL("CREATE TABLE " + TABLE_HISTORY + "(" +
+                    History._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    History.TITLE + " TEXT," +
+                    History.URL + " TEXT NOT NULL," +
+                    History.DATE_CREATED + " INTEGER," +
+                    History.DATE_LAST_VISITED + " INTEGER," +
+                    History.VISITS + " INTEGER NOT NULL DEFAULT 0," +
+                    History.USER_ENTERED + " INTEGER" +
+                    ");");
+
+            db.execSQL("CREATE TABLE " + TABLE_IMAGES + " (" +
+                    Images.URL + " TEXT UNIQUE NOT NULL," +
+                    Images.FAVICON + " BLOB," +
+                    Images.THUMBNAIL + " BLOB," +
+                    Images.TOUCH_ICON + " BLOB" +
+                    ");");
+            db.execSQL("CREATE INDEX imagesUrlIndex ON " + TABLE_IMAGES +
+                    "(" + Images.URL + ")");
+
+            db.execSQL("CREATE TABLE " + TABLE_SEARCHES + " (" +
+                    Searches._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    Searches.SEARCH + " TEXT," +
+                    Searches.DATE + " LONG" +
+                    ");");
+
+            db.execSQL("CREATE TABLE " + TABLE_SETTINGS + " (" +
+                    Settings.KEY + " TEXT PRIMARY KEY," +
+                    Settings.VALUE + " TEXT NOT NULL" +
+                    ");");
+
+            createAccountsView(db);
+            createThumbnails(db);
+
+            mSyncHelper.createDatabase(db);
+
+            if (!importFromBrowserProvider(db)) {
+                createDefaultBookmarks(db);
+            }
+
+            enableSync(db);
+            createOmniboxSuggestions(db);
+        }
+
+        void createOmniboxSuggestions(SQLiteDatabase db) {
+            db.execSQL(SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS);
+        }
+
+        void createThumbnails(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE IF NOT EXISTS " + TABLE_THUMBNAILS + " (" +
+                    Thumbnails._ID + " INTEGER PRIMARY KEY," +
+                    Thumbnails.THUMBNAIL + " BLOB NOT NULL" +
+                    ");");
+        }
+
+        void enableSync(SQLiteDatabase db) {
+            ContentValues values = new ContentValues();
+            values.put(Settings.KEY, Settings.KEY_SYNC_ENABLED);
+            values.put(Settings.VALUE, 1);
+            insertSettingsInTransaction(db, values);
+            // Enable bookmark sync on all accounts
+            AccountManager am = (AccountManager) getContext().getSystemService(
+                    Context.ACCOUNT_SERVICE);
+            if (am == null) {
+                return;
+            }
+            Account[] accounts = am.getAccountsByType("com.google");
+            if (accounts == null || accounts.length == 0) {
+                return;
+            }
+            for (Account account : accounts) {
+                if (ContentResolver.getIsSyncable(
+                        account, BrowserContract.AUTHORITY) == 0) {
+                    // Account wasn't syncable, enable it
+                    ContentResolver.setIsSyncable(
+                            account, BrowserContract.AUTHORITY, 1);
+                    ContentResolver.setSyncAutomatically(
+                            account, BrowserContract.AUTHORITY, true);
+                }
+            }
+        }
+
+        boolean importFromBrowserProvider(SQLiteDatabase db) {
+            Context context = getContext();
+            File oldDbFile = context.getDatabasePath(BrowserProvider.sDatabaseName);
+            if (oldDbFile.exists()) {
+                BrowserProvider.DatabaseHelper helper =
+                        new BrowserProvider.DatabaseHelper(context);
+                SQLiteDatabase oldDb = helper.getWritableDatabase();
+                Cursor c = null;
+                try {
+                    String table = BrowserProvider.TABLE_NAMES[BrowserProvider.URI_MATCH_BOOKMARKS];
+                    // Import bookmarks
+                    c = oldDb.query(table,
+                            new String[] {
+                            BookmarkColumns.URL, // 0
+                            BookmarkColumns.TITLE, // 1
+                            BookmarkColumns.FAVICON, // 2
+                            BookmarkColumns.TOUCH_ICON, // 3
+                            BookmarkColumns.CREATED, // 4
+                            }, BookmarkColumns.BOOKMARK + "!=0", null,
+                            null, null, null);
+                    if (c != null) {
+                        while (c.moveToNext()) {
+                            String url = c.getString(0);
+                            if (TextUtils.isEmpty(url))
+                                continue; // We require a valid URL
+                            ContentValues values = new ContentValues();
+                            values.put(Bookmarks.URL, url);
+                            values.put(Bookmarks.TITLE, c.getString(1));
+                            values.put(Bookmarks.DATE_CREATED, c.getInt(4));
+                            values.put(Bookmarks.POSITION, 0);
+                            values.put(Bookmarks.PARENT, FIXED_ID_ROOT);
+                            ContentValues imageValues = new ContentValues();
+                            imageValues.put(Images.URL, url);
+                            imageValues.put(Images.FAVICON, c.getBlob(2));
+                            imageValues.put(Images.TOUCH_ICON, c.getBlob(3));
+                            db.insert(TABLE_IMAGES, Images.THUMBNAIL, imageValues);
+                            db.insert(TABLE_BOOKMARKS, Bookmarks.DIRTY, values);
+                        }
+                        c.close();
+                    }
+                    // Import history
+                    c = oldDb.query(table,
+                            new String[] {
+                            BookmarkColumns.URL, // 0
+                            BookmarkColumns.TITLE, // 1
+                            BookmarkColumns.VISITS, // 2
+                            BookmarkColumns.DATE, // 3
+                            BookmarkColumns.CREATED, // 4
+                            }, BookmarkColumns.VISITS + " > 0 OR "
+                            + BookmarkColumns.BOOKMARK + " = 0",
+                            null, null, null, null);
+                    if (c != null) {
+                        while (c.moveToNext()) {
+                            ContentValues values = new ContentValues();
+                            String url = c.getString(0);
+                            if (TextUtils.isEmpty(url))
+                                continue; // We require a valid URL
+                            values.put(History.URL, url);
+                            values.put(History.TITLE, c.getString(1));
+                            values.put(History.VISITS, c.getInt(2));
+                            values.put(History.DATE_LAST_VISITED, c.getLong(3));
+                            values.put(History.DATE_CREATED, c.getLong(4));
+                            db.insert(TABLE_HISTORY, History.FAVICON, values);
+                        }
+                        c.close();
+                    }
+                    // Wipe the old DB, in case the delete fails.
+                    oldDb.delete(table, null, null);
+                } finally {
+                    if (c != null) c.close();
+                    oldDb.close();
+                    helper.close();
+                }
+                if (!oldDbFile.delete()) {
+                    oldDbFile.deleteOnExit();
+                }
+                return true;
+            }
+            return false;
+        }
+
+        void createAccountsView(SQLiteDatabase db) {
+            db.execSQL("CREATE VIEW IF NOT EXISTS v_accounts AS "
+                    + "SELECT NULL AS " + Accounts.ACCOUNT_NAME
+                    + ", NULL AS " + Accounts.ACCOUNT_TYPE
+                    + ", " + FIXED_ID_ROOT + " AS " + Accounts.ROOT_ID
+                    + " UNION ALL SELECT " + Accounts.ACCOUNT_NAME
+                    + ", " + Accounts.ACCOUNT_TYPE + ", "
+                    + Bookmarks._ID + " AS " + Accounts.ROOT_ID
+                    + " FROM " + TABLE_BOOKMARKS + " WHERE "
+                    + ChromeSyncColumns.SERVER_UNIQUE + " = \""
+                    + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "\" AND "
+                    + Bookmarks.IS_DELETED + " = 0");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion < 32) {
+                createOmniboxSuggestions(db);
+            }
+            if (oldVersion < 31) {
+                createThumbnails(db);
+            }
+            if (oldVersion < 30) {
+                db.execSQL("DROP VIEW IF EXISTS " + VIEW_SNAPSHOTS_COMBINED);
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_SNAPSHOTS);
+            }
+            if (oldVersion < 28) {
+                enableSync(db);
+            }
+            if (oldVersion < 27) {
+                createAccountsView(db);
+            }
+            if (oldVersion < 26) {
+                db.execSQL("DROP VIEW IF EXISTS combined");
+            }
+            if (oldVersion < 25) {
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOKMARKS);
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_HISTORY);
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_SEARCHES);
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES);
+                db.execSQL("DROP TABLE IF EXISTS " + TABLE_SETTINGS);
+                mSyncHelper.onAccountsChanged(db, new Account[] {}); // remove all sync info
+                onCreate(db);
+            }
+        }
+
+        public void onOpen(SQLiteDatabase db) {
+            mSyncHelper.onDatabaseOpened(db);
+        }
+
+        private void createDefaultBookmarks(SQLiteDatabase db) {
+            ContentValues values = new ContentValues();
+            // TODO figure out how to deal with localization for the defaults
+
+            // Bookmarks folder
+            values.put(Bookmarks._ID, FIXED_ID_ROOT);
+            values.put(ChromeSyncColumns.SERVER_UNIQUE, ChromeSyncColumns.FOLDER_NAME_BOOKMARKS);
+            values.put(Bookmarks.TITLE, "Bookmarks");
+            values.putNull(Bookmarks.PARENT);
+            values.put(Bookmarks.POSITION, 0);
+            values.put(Bookmarks.IS_FOLDER, true);
+            values.put(Bookmarks.DIRTY, true);
+            db.insertOrThrow(TABLE_BOOKMARKS, null, values);
+
+            // add for carrier bookmark feature
+            Object[] params  = { new String("persist.env.c.browser.resource"),
+                                             new String("default")};
+                        Class[] type = new Class[] {String.class, String.class};
+                        String browserRes = (String)ReflectHelper.invokeStaticMethod(
+                                    "android.os.SystemProperties", "get",
+                                    type, params);
+
+            //don't add default bookmarks for cmcc
+            if (!"cmcc".equals(browserRes)) {
+                addDefaultBookmarks(db, FIXED_ID_ROOT);
+            }
+            if ("ct".equals(browserRes) || "cmcc".equals(browserRes)) {
+                addDefaultCarrierBookmarks(db, FIXED_ID_ROOT);
+            }
+        }
+
+        private void addDefaultBookmarks(SQLiteDatabase db, long parentId) {
+            Resources res = getContext().getResources();
+            final CharSequence[] bookmarks = res.getTextArray(
+                    R.array.bookmarks);
+            int size = bookmarks.length;
+            TypedArray preloads = res.obtainTypedArray(R.array.bookmark_preloads);
+            try {
+                String parent = Long.toString(parentId);
+                String now = Long.toString(System.currentTimeMillis());
+                for (int i = 0; i < size; i = i + 2) {
+                    CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(),
+                            bookmarks[i + 1]);
+                    db.execSQL("INSERT INTO bookmarks (" +
+                            Bookmarks.TITLE + ", " +
+                            Bookmarks.URL + ", " +
+                            Bookmarks.IS_FOLDER + "," +
+                            Bookmarks.PARENT + "," +
+                            Bookmarks.POSITION + "," +
+                            Bookmarks.DATE_CREATED +
+                        ") VALUES (" +
+                            "'" + bookmarks[i] + "', " +
+                            "'" + bookmarkDestination + "', " +
+                            "0," +
+                            parent + "," +
+                            Integer.toString(i) + "," +
+                            now +
+                            ");");
+
+                    int faviconId = preloads.getResourceId(i, 0);
+                    int thumbId = preloads.getResourceId(i + 1, 0);
+                    byte[] thumb = null, favicon = null;
+                    try {
+                        thumb = readRaw(res, thumbId);
+                    } catch (IOException e) {
+                    }
+                    try {
+                        favicon = readRaw(res, faviconId);
+                    } catch (IOException e) {
+                    }
+                    if (thumb != null || favicon != null) {
+                        ContentValues imageValues = new ContentValues();
+                        imageValues.put(Images.URL, bookmarkDestination.toString());
+                        if (favicon != null) {
+                            imageValues.put(Images.FAVICON, favicon);
+                        }
+                        if (thumb != null) {
+                            imageValues.put(Images.THUMBNAIL, thumb);
+                        }
+                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
+                    }
+                }
+            } catch (ArrayIndexOutOfBoundsException e) {
+            } finally {
+                preloads.recycle();
+            }
+        }
+
+        // add for carrier bookmark feature
+        private void addDefaultCarrierBookmarks(SQLiteDatabase db, long parentId) {
+            Context mResPackageCtx = null;
+            try {
+                mResPackageCtx = getContext().createPackageContext(
+                    "com.android.browser.res",
+                    Context.CONTEXT_IGNORE_SECURITY);
+            } catch (Exception e) {
+                Log.e(TAG, "Create Res Apk Failed");
+            }
+            if (mResPackageCtx == null)
+                return;
+
+            CharSequence[] bookmarks = null;
+            TypedArray preloads = null;
+            Resources res = mResPackageCtx.getResources();
+            int resBookmarksID = res.getIdentifier("bookmarks",
+                                                   "array",
+                                                   "com.android.browser.res");
+            int resPreloadsID = res.getIdentifier("bookmark_preloads", "array",
+                    "com.android.browser.res");
+            if (resBookmarksID != 0 && resPreloadsID != 0) {
+                bookmarks = res.getTextArray(resBookmarksID);
+                preloads = res.obtainTypedArray(resPreloadsID);
+            } else {
+                return;
+            }
+
+            // The Default Carrier bookmarks size
+            int size = bookmarks.length;
+
+            // googleSize the Default Google bookmarks size.
+            // The Default Carrier Bookmarks original position need move to googleSize index.
+            final CharSequence[] googleBookmarks = getContext().getResources().getTextArray(
+                    R.array.bookmarks);
+            int googleSize = googleBookmarks.length;
+            try {
+                String parent = Long.toString(parentId);
+                String now = Long.toString(System.currentTimeMillis());
+                for (int i = 0; i < size; i = i + 2) {
+                    CharSequence bookmarkDestination = replaceSystemPropertyInString(getContext(),
+                            bookmarks[i + 1]);
+                    db.execSQL("INSERT INTO bookmarks (" +
+                            Bookmarks.TITLE + ", " +
+                            Bookmarks.URL + ", " +
+                            Bookmarks.IS_FOLDER + "," +
+                            Bookmarks.PARENT + "," +
+                            Bookmarks.POSITION + "," +
+                            Bookmarks.DATE_CREATED +
+                        ") VALUES (" +
+                            "'" + bookmarks[i] + "', " +
+                            "'" + bookmarkDestination + "', " +
+                            "0," +
+                            parent + "," +
+                            Integer.toString(googleSize + i) + "," +
+                            now +
+                            ");");
+
+                    int faviconId = preloads.getResourceId(i, 0);
+                    int thumbId = preloads.getResourceId(i + 1, 0);
+                    byte[] thumb = null, favicon = null;
+                    try {
+                        thumb = readRaw(res, thumbId);
+                    } catch (IOException e) {
+                    }
+                    try {
+                        favicon = readRaw(res, faviconId);
+                    } catch (IOException e) {
+                    }
+                    if (thumb != null || favicon != null) {
+                        ContentValues imageValues = new ContentValues();
+                        imageValues.put(Images.URL, bookmarkDestination.toString());
+                        if (favicon != null) {
+                            imageValues.put(Images.FAVICON, favicon);
+                        }
+                        if (thumb != null) {
+                            imageValues.put(Images.THUMBNAIL, thumb);
+                        }
+                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
+                    }
+                }
+            } catch (ArrayIndexOutOfBoundsException e) {
+            } finally {
+                preloads.recycle();
+            }
+        }
+
+        private byte[] readRaw(Resources res, int id) throws IOException {
+            if (id == 0) {
+                return null;
+            }
+            InputStream is = res.openRawResource(id);
+            try {
+                ByteArrayOutputStream bos = new ByteArrayOutputStream();
+                byte[] buf = new byte[4096];
+                int read;
+                while ((read = is.read(buf)) > 0) {
+                    bos.write(buf, 0, read);
+                }
+                bos.flush();
+                return bos.toByteArray();
+            } finally {
+                is.close();
+            }
+        }
+
+        // XXX: This is a major hack to remove our dependency on gsf constants and
+        // its content provider. http://b/issue?id=2425179
+        private String getClientId(ContentResolver cr) {
+            String ret = "android-google";
+            Cursor c = null;
+            try {
+                c = cr.query(Uri.parse("content://com.google.settings/partner"),
+                        new String[] { "value" }, "name='client_id'", null, null);
+                if (c != null && c.moveToNext()) {
+                    ret = c.getString(0);
+                }
+            } catch (RuntimeException ex) {
+                // fall through to return the default
+            } finally {
+                if (c != null) {
+                    c.close();
+                }
+            }
+            return ret;
+        }
+
+        private CharSequence replaceSystemPropertyInString(Context context, CharSequence srcString){
+            StringBuffer sb = new StringBuffer();
+            int lastCharLoc = 0;
+
+            final String client_id = getClientId(context.getContentResolver());
+
+            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();
+                            if (propertyKeyValue.equals("CLIENT_ID")) {
+                                sb.append(client_id);
+                            } else {
+                                sb.append("unknown");
+                            }
+                            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;
+        }
+    }
+
+    @Override
+    public SQLiteOpenHelper getDatabaseHelper(Context context) {
+        synchronized (this) {
+            if (mOpenHelper == null) {
+                mOpenHelper = new DatabaseHelper(context);
+            }
+            return mOpenHelper;
+        }
+    }
+
+    @Override
+    public boolean isCallerSyncAdapter(Uri uri) {
+        return uri.getBooleanQueryParameter(BrowserContract.CALLER_IS_SYNCADAPTER, false);
+    }
+
+    @VisibleForTesting
+    public void setWidgetObserver(ContentObserver obs) {
+        mWidgetObserver = obs;
+    }
+
+    void refreshWidgets() {
+        mUpdateWidgets = true;
+    }
+
+    @Override
+    protected void onEndTransaction(boolean callerIsSyncAdapter) {
+        super.onEndTransaction(callerIsSyncAdapter);
+        if (mUpdateWidgets) {
+            if (mWidgetObserver == null) {
+                BookmarkThumbnailWidgetProvider.refreshWidgets(getContext());
+            } else {
+                mWidgetObserver.dispatchChange(false);
+            }
+            mUpdateWidgets = false;
+        }
+        mSyncToNetwork = true;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        final int match = URI_MATCHER.match(uri);
+        switch (match) {
+            case LEGACY:
+            case BOOKMARKS:
+                return Bookmarks.CONTENT_TYPE;
+            case LEGACY_ID:
+            case BOOKMARKS_ID:
+                return Bookmarks.CONTENT_ITEM_TYPE;
+            case HISTORY:
+                return History.CONTENT_TYPE;
+            case HISTORY_ID:
+                return History.CONTENT_ITEM_TYPE;
+            case SEARCHES:
+                return Searches.CONTENT_TYPE;
+            case SEARCHES_ID:
+                return Searches.CONTENT_ITEM_TYPE;
+        }
+        return null;
+    }
+
+    boolean isNullAccount(String account) {
+        if (account == null) return true;
+        account = account.trim();
+        return account.length() == 0 || account.equals("null");
+    }
+
+    Object[] getSelectionWithAccounts(Uri uri, String selection, String[] selectionArgs) {
+        // Look for account info
+        String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
+        String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
+        boolean hasAccounts = false;
+        if (accountType != null && accountName != null) {
+            if (!isNullAccount(accountType) && !isNullAccount(accountName)) {
+                selection = DatabaseUtils.concatenateWhere(selection,
+                        Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=? ");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { accountType, accountName });
+                hasAccounts = true;
+            } else {
+                selection = DatabaseUtils.concatenateWhere(selection,
+                        Bookmarks.ACCOUNT_NAME + " IS NULL AND " +
+                        Bookmarks.ACCOUNT_TYPE + " IS NULL");
+            }
+        }
+        return new Object[] { selection, selectionArgs, hasAccounts };
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+            String sortOrder) {
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        final int match = URI_MATCHER.match(uri);
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+        String groupBy = uri.getQueryParameter(PARAM_GROUP_BY);
+        switch (match) {
+            case ACCOUNTS: {
+                qb.setTables(VIEW_ACCOUNTS);
+                qb.setProjectionMap(ACCOUNTS_PROJECTION_MAP);
+                String allowEmpty = uri.getQueryParameter(PARAM_ALLOW_EMPTY_ACCOUNTS);
+                if ("false".equals(allowEmpty)) {
+                    selection = DatabaseUtils.concatenateWhere(selection,
+                            SQL_WHERE_ACCOUNT_HAS_BOOKMARKS);
+                }
+                if (sortOrder == null) {
+                    sortOrder = DEFAULT_SORT_ACCOUNTS;
+                }
+                break;
+            }
+
+            case BOOKMARKS_FOLDER_ID:
+            case BOOKMARKS_ID:
+            case BOOKMARKS: {
+                // Only show deleted bookmarks if requested to do so
+                if (!uri.getBooleanQueryParameter(Bookmarks.QUERY_PARAMETER_SHOW_DELETED, false)){
+                    selection = DatabaseUtils.concatenateWhere(
+                            Bookmarks.IS_DELETED + "=0", selection);
+                }
+
+                if (match == BOOKMARKS_ID) {
+                    // Tack on the ID of the specific bookmark requested
+                    selection = DatabaseUtils.concatenateWhere(selection,
+                            TABLE_BOOKMARKS + "." + Bookmarks._ID + "=?");
+                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                            new String[] { Long.toString(ContentUris.parseId(uri)) });
+                } else if (match == BOOKMARKS_FOLDER_ID) {
+                    // Tack on the ID of the specific folder requested
+                    selection = DatabaseUtils.concatenateWhere(selection,
+                            TABLE_BOOKMARKS + "." + Bookmarks.PARENT + "=?");
+                    selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                            new String[] { Long.toString(ContentUris.parseId(uri)) });
+                }
+
+                Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
+                selection = (String) withAccount[0];
+                selectionArgs = (String[]) withAccount[1];
+                boolean hasAccounts = (Boolean) withAccount[2];
+
+                // Set a default sort order if one isn't specified
+                if (TextUtils.isEmpty(sortOrder)) {
+                    if (hasAccounts) {
+                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC;
+                    } else {
+                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
+                    }
+                }
+
+                qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
+                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
+                break;
+            }
+
+            case BOOKMARKS_FOLDER: {
+                // Look for an account
+                boolean useAccount = false;
+                String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
+                String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
+                if (!isNullAccount(accountType) && !isNullAccount(accountName)) {
+                    useAccount = true;
+                }
+
+                qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
+                String[] args;
+                String query;
+                // Set a default sort order if one isn't specified
+                if (TextUtils.isEmpty(sortOrder)) {
+                    if (useAccount) {
+                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER_SYNC;
+                    } else {
+                        sortOrder = DEFAULT_BOOKMARKS_SORT_ORDER;
+                    }
+                }
+                if (!useAccount) {
+                    qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
+                    String where = Bookmarks.PARENT + "=? AND " + Bookmarks.IS_DELETED + "=0";
+                    where = DatabaseUtils.concatenateWhere(where, selection);
+                    args = new String[] { Long.toString(FIXED_ID_ROOT) };
+                    if (selectionArgs != null) {
+                        args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+                    }
+                    query = qb.buildQuery(projection, where, null, null, sortOrder, null);
+                } else {
+                    qb.setProjectionMap(BOOKMARKS_PROJECTION_MAP);
+                    String where = Bookmarks.ACCOUNT_TYPE + "=? AND " +
+                            Bookmarks.ACCOUNT_NAME + "=? " +
+                            "AND parent = " +
+                            "(SELECT _id FROM " + TABLE_BOOKMARKS + " WHERE " +
+                            ChromeSyncColumns.SERVER_UNIQUE + "=" +
+                            "'" + ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR + "' " +
+                            "AND account_type = ? AND account_name = ?) " +
+                            "AND " + Bookmarks.IS_DELETED + "=0";
+                    where = DatabaseUtils.concatenateWhere(where, selection);
+                    String bookmarksBarQuery = qb.buildQuery(projection,
+                            where, null, null, null, null);
+                    args = new String[] {accountType, accountName,
+                            accountType, accountName};
+                    if (selectionArgs != null) {
+                        args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+                    }
+
+                    where = Bookmarks.ACCOUNT_TYPE + "=? AND " + Bookmarks.ACCOUNT_NAME + "=?" +
+                            " AND " + ChromeSyncColumns.SERVER_UNIQUE + "=?";
+                    where = DatabaseUtils.concatenateWhere(where, selection);
+                    qb.setProjectionMap(OTHER_BOOKMARKS_PROJECTION_MAP);
+                    String otherBookmarksQuery = qb.buildQuery(projection,
+                            where, null, null, null, null);
+
+                    query = qb.buildUnionQuery(
+                            new String[] { bookmarksBarQuery, otherBookmarksQuery },
+                            sortOrder, limit);
+
+                    args = DatabaseUtils.appendSelectionArgs(args, new String[] {
+                            accountType, accountName, ChromeSyncColumns.FOLDER_NAME_OTHER_BOOKMARKS,
+                            });
+                    if (selectionArgs != null) {
+                        args = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+                    }
+                }
+
+                Cursor cursor = db.rawQuery(query, args);
+                if (cursor != null) {
+                    cursor.setNotificationUri(getContext().getContentResolver(),
+                            BrowserContract.AUTHORITY_URI);
+                }
+                return cursor;
+            }
+
+            case BOOKMARKS_DEFAULT_FOLDER_ID: {
+                String accountName = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_NAME);
+                String accountType = uri.getQueryParameter(Bookmarks.PARAM_ACCOUNT_TYPE);
+                long id = queryDefaultFolderId(accountName, accountType);
+                MatrixCursor c = new MatrixCursor(new String[] {Bookmarks._ID});
+                c.newRow().add(id);
+                return c;
+            }
+
+            case BOOKMARKS_SUGGESTIONS: {
+                return doSuggestQuery(selection, selectionArgs, limit);
+            }
+
+            case HISTORY_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case HISTORY: {
+                filterSearchClient(selectionArgs);
+                if (sortOrder == null) {
+                    sortOrder = DEFAULT_SORT_HISTORY;
+                }
+                qb.setProjectionMap(HISTORY_PROJECTION_MAP);
+                qb.setTables(TABLE_HISTORY_JOIN_IMAGES);
+                break;
+            }
+
+            case SEARCHES_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case SEARCHES: {
+                qb.setTables(TABLE_SEARCHES);
+                qb.setProjectionMap(SEARCHES_PROJECTION_MAP);
+                break;
+            }
+
+            case SYNCSTATE: {
+                return mSyncHelper.query(db, projection, selection, selectionArgs, sortOrder);
+            }
+
+            case SYNCSTATE_ID: {
+                selection = appendAccountToSelection(uri, selection);
+                String selectionWithId =
+                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+                        + (selection == null ? "" : " AND (" + selection + ")");
+                return mSyncHelper.query(db, projection, selectionWithId, selectionArgs, sortOrder);
+            }
+
+            case IMAGES: {
+                qb.setTables(TABLE_IMAGES);
+                qb.setProjectionMap(IMAGES_PROJECTION_MAP);
+                break;
+            }
+
+            case LEGACY_ID:
+            case COMBINED_ID: {
+                selection = DatabaseUtils.concatenateWhere(
+                        selection, Combined._ID + " = CAST(? AS INTEGER)");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case LEGACY:
+            case COMBINED: {
+                if ((match == LEGACY || match == LEGACY_ID)
+                        && projection == null) {
+                    projection = Browser.HISTORY_PROJECTION;
+                }
+                String[] args = createCombinedQuery(uri, projection, qb);
+                if (selectionArgs == null) {
+                    selectionArgs = args;
+                } else {
+                    selectionArgs = DatabaseUtils.appendSelectionArgs(args, selectionArgs);
+                }
+                break;
+            }
+
+            case SETTINGS: {
+                qb.setTables(TABLE_SETTINGS);
+                qb.setProjectionMap(SETTINGS_PROJECTION_MAP);
+                break;
+            }
+
+            case THUMBNAILS_ID: {
+                selection = DatabaseUtils.concatenateWhere(
+                        selection, Thumbnails._ID + " = ?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case THUMBNAILS: {
+                qb.setTables(TABLE_THUMBNAILS);
+                break;
+            }
+
+            case OMNIBOX_SUGGESTIONS: {
+                qb.setTables(VIEW_OMNIBOX_SUGGESTIONS);
+                break;
+            }
+
+            case HOMEPAGE: {
+                String homepage = BrowserSettings.getInstance().getHomePage();
+                Log.d(TAG,"get home page for DM");
+                if (null == homepage) {
+                    return null;
+                }
+                String arrColumns[] = {"homepage"};
+                String arrHomepage[] = {homepage};
+                MatrixCursor matrixCursor = new MatrixCursor(arrColumns, 1);
+                matrixCursor.addRow(arrHomepage);
+                return matrixCursor;
+            }
+
+            default: {
+                throw new UnsupportedOperationException("Unknown URL " + uri.toString());
+            }
+        }
+
+        Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy,
+                null, sortOrder, limit);
+        cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.AUTHORITY_URI);
+        return cursor;
+    }
+
+    private Cursor doSuggestQuery(String selection, String[] selectionArgs, String limit) {
+        if (TextUtils.isEmpty(selectionArgs[0])) {
+            selection = ZERO_QUERY_SUGGEST_SELECTION;
+            selectionArgs = null;
+        } else {
+            String like = selectionArgs[0] + "%";
+            if (selectionArgs[0].startsWith("http")
+                    || selectionArgs[0].startsWith("file")) {
+                selectionArgs[0] = like;
+            } else {
+                selectionArgs = new String[6];
+                selectionArgs[0] = "http://" + like;
+                selectionArgs[1] = "http://www." + like;
+                selectionArgs[2] = "https://" + like;
+                selectionArgs[3] = "https://www." + like;
+                // To match against titles.
+                selectionArgs[4] = like;
+                selectionArgs[5] = like;
+                selection = SUGGEST_SELECTION;
+            }
+            selection = DatabaseUtils.concatenateWhere(selection,
+                    Bookmarks.IS_DELETED + "=0 AND " + Bookmarks.IS_FOLDER + "=0");
+
+        }
+        Cursor c = mOpenHelper.getReadableDatabase().query(TABLE_BOOKMARKS_JOIN_HISTORY,
+                SUGGEST_PROJECTION, selection, selectionArgs, null, null,
+                null, null);
+
+        return new SuggestionsCursor(c);
+    }
+
+    private String[] createCombinedQuery(
+            Uri uri, String[] projection, SQLiteQueryBuilder qb) {
+        String[] args = null;
+        StringBuilder whereBuilder = new StringBuilder(128);
+        whereBuilder.append(Bookmarks.IS_DELETED);
+        whereBuilder.append(" = 0");
+        // Look for account info
+        Object[] withAccount = getSelectionWithAccounts(uri, null, null);
+        String selection = (String) withAccount[0];
+        String[] selectionArgs = (String[]) withAccount[1];
+        if (selection != null) {
+            whereBuilder.append(" AND " + selection);
+            if (selectionArgs != null) {
+                // We use the selection twice, hence we need to duplicate the args
+                args = new String[selectionArgs.length * 2];
+                System.arraycopy(selectionArgs, 0, args, 0, selectionArgs.length);
+                System.arraycopy(selectionArgs, 0, args, selectionArgs.length,
+                        selectionArgs.length);
+            }
+        }
+        String where = whereBuilder.toString();
+        // Build the bookmark subquery for history union subquery
+        qb.setTables(TABLE_BOOKMARKS);
+        String subQuery = qb.buildQuery(null, where, null, null, null, null);
+        // Build the history union subquery
+        qb.setTables(String.format(FORMAT_COMBINED_JOIN_SUBQUERY_JOIN_IMAGES, subQuery));
+        qb.setProjectionMap(COMBINED_HISTORY_PROJECTION_MAP);
+        String historySubQuery = qb.buildQuery(null,
+                null, null, null, null, null);
+        // Build the bookmark union subquery
+        qb.setTables(TABLE_BOOKMARKS_JOIN_IMAGES);
+        qb.setProjectionMap(COMBINED_BOOKMARK_PROJECTION_MAP);
+        where += String.format(" AND %s NOT IN (SELECT %s FROM %s)",
+                Combined.URL, History.URL, TABLE_HISTORY);
+        String bookmarksSubQuery = qb.buildQuery(null, where,
+                null, null, null, null);
+        // Put it all together
+        String query = qb.buildUnionQuery(
+                new String[] {historySubQuery, bookmarksSubQuery},
+                null, null);
+        qb.setTables("(" + query + ")");
+        qb.setProjectionMap(null);
+        return args;
+    }
+
+    int deleteBookmarks(String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter) {
+        //TODO cascade deletes down from folders
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        if (callerIsSyncAdapter) {
+            return db.delete(TABLE_BOOKMARKS, selection, selectionArgs);
+        }
+        ContentValues values = new ContentValues();
+        values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
+        values.put(Bookmarks.IS_DELETED, 1);
+        return updateBookmarksInTransaction(values, selection, selectionArgs,
+                callerIsSyncAdapter);
+    }
+
+    @Override
+    public int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter) {
+        final int match = URI_MATCHER.match(uri);
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int deleted = 0;
+        switch (match) {
+            case BOOKMARKS_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection,
+                        TABLE_BOOKMARKS + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case BOOKMARKS: {
+                // Look for account info
+                Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
+                selection = (String) withAccount[0];
+                selectionArgs = (String[]) withAccount[1];
+                deleted = deleteBookmarks(selection, selectionArgs, callerIsSyncAdapter);
+                pruneImages();
+                if (deleted > 0) {
+                    refreshWidgets();
+                }
+                break;
+            }
+
+            case HISTORY_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case HISTORY: {
+                filterSearchClient(selectionArgs);
+                deleted = db.delete(TABLE_HISTORY, selection, selectionArgs);
+                pruneImages();
+                break;
+            }
+
+            case SEARCHES_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection, TABLE_SEARCHES + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case SEARCHES: {
+                deleted = db.delete(TABLE_SEARCHES, selection, selectionArgs);
+                break;
+            }
+
+            case SYNCSTATE: {
+                deleted = mSyncHelper.delete(db, selection, selectionArgs);
+                break;
+            }
+            case SYNCSTATE_ID: {
+                String selectionWithId =
+                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+                        + (selection == null ? "" : " AND (" + selection + ")");
+                deleted = mSyncHelper.delete(db, selectionWithId, selectionArgs);
+                break;
+            }
+            case LEGACY_ID: {
+                selection = DatabaseUtils.concatenateWhere(
+                        selection, Combined._ID + " = CAST(? AS INTEGER)");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case LEGACY: {
+                String[] projection = new String[] { Combined._ID,
+                        Combined.IS_BOOKMARK, Combined.URL };
+                SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+                String[] args = createCombinedQuery(uri, projection, qb);
+                if (selectionArgs == null) {
+                    selectionArgs = args;
+                } else {
+                    selectionArgs = DatabaseUtils.appendSelectionArgs(
+                            args, selectionArgs);
+                }
+                Cursor c = qb.query(db, projection, selection, selectionArgs,
+                        null, null, null);
+                while (c.moveToNext()) {
+                    long id = c.getLong(0);
+                    boolean isBookmark = c.getInt(1) != 0;
+                    String url = c.getString(2);
+                    if (isBookmark) {
+                        deleted += deleteBookmarks(Bookmarks._ID + "=?",
+                                new String[] { Long.toString(id) },
+                                callerIsSyncAdapter);
+                        db.delete(TABLE_HISTORY, History.URL + "=?",
+                                new String[] { url });
+                    } else {
+                        deleted += db.delete(TABLE_HISTORY,
+                                Bookmarks._ID + "=?",
+                                new String[] { Long.toString(id) });
+                    }
+                }
+                c.close();
+                break;
+            }
+            case THUMBNAILS_ID: {
+                selection = DatabaseUtils.concatenateWhere(
+                        selection, Thumbnails._ID + " = ?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case THUMBNAILS: {
+                deleted = db.delete(TABLE_THUMBNAILS, selection, selectionArgs);
+                break;
+            }
+            default: {
+                throw new UnsupportedOperationException("Unknown delete URI " + uri);
+            }
+        }
+        if (deleted > 0) {
+            postNotifyUri(uri);
+            if (shouldNotifyLegacy(uri)) {
+                postNotifyUri(LEGACY_AUTHORITY_URI);
+            }
+        }
+        return deleted;
+    }
+
+    long queryDefaultFolderId(String accountName, String accountType) {
+        if (!isNullAccount(accountName) && !isNullAccount(accountType)) {
+            final SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+            Cursor c = db.query(TABLE_BOOKMARKS, new String[] { Bookmarks._ID },
+                    ChromeSyncColumns.SERVER_UNIQUE + " = ?" +
+                    " AND account_type = ? AND account_name = ?",
+                    new String[] { ChromeSyncColumns.FOLDER_NAME_BOOKMARKS_BAR,
+                    accountType, accountName }, null, null, null);
+            try {
+                if (c.moveToFirst()) {
+                    return c.getLong(0);
+                }
+            } finally {
+                c.close();
+            }
+        }
+        return FIXED_ID_ROOT;
+    }
+
+    @Override
+    public Uri insertInTransaction(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
+        int match = URI_MATCHER.match(uri);
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        long id = -1;
+        if (match == LEGACY) {
+            // Intercept and route to the correct table
+            Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK);
+            values.remove(BookmarkColumns.BOOKMARK);
+            if (bookmark == null || bookmark == 0) {
+                match = HISTORY;
+            } else {
+                match = BOOKMARKS;
+                values.remove(BookmarkColumns.DATE);
+                values.remove(BookmarkColumns.VISITS);
+                values.remove(BookmarkColumns.USER_ENTERED);
+                values.put(Bookmarks.IS_FOLDER, 0);
+            }
+        }
+        switch (match) {
+            case BOOKMARKS: {
+                // Mark rows dirty if they're not coming from a sync adapter
+                if (!callerIsSyncAdapter) {
+                    long now = System.currentTimeMillis();
+                    values.put(Bookmarks.DATE_CREATED, now);
+                    values.put(Bookmarks.DATE_MODIFIED, now);
+                    values.put(Bookmarks.DIRTY, 1);
+
+                    boolean hasAccounts = values.containsKey(Bookmarks.ACCOUNT_TYPE)
+                            || values.containsKey(Bookmarks.ACCOUNT_NAME);
+                    String accountType = values
+                            .getAsString(Bookmarks.ACCOUNT_TYPE);
+                    String accountName = values
+                            .getAsString(Bookmarks.ACCOUNT_NAME);
+                    boolean hasParent = values.containsKey(Bookmarks.PARENT);
+                    if (hasParent && hasAccounts) {
+                        // Let's make sure it's valid
+                        long parentId = values.getAsLong(Bookmarks.PARENT);
+                        hasParent = isValidParent(
+                                accountType, accountName, parentId);
+                    } else if (hasParent && !hasAccounts) {
+                        long parentId = values.getAsLong(Bookmarks.PARENT);
+                        hasParent = setParentValues(parentId, values);
+                    }
+
+                    // If no parent is set default to the "Bookmarks Bar" folder
+                    if (!hasParent) {
+                        values.put(Bookmarks.PARENT,
+                                queryDefaultFolderId(accountName, accountType));
+                    }
+                }
+
+                // If no position is requested put the bookmark at the beginning of the list
+                if (!values.containsKey(Bookmarks.POSITION)) {
+                    values.put(Bookmarks.POSITION, Long.toString(Long.MIN_VALUE));
+                }
+
+                // Extract out the image values so they can be inserted into the images table
+                String url = values.getAsString(Bookmarks.URL);
+                ContentValues imageValues = extractImageValues(values, url);
+                Boolean isFolder = values.getAsBoolean(Bookmarks.IS_FOLDER);
+                if ((isFolder == null || !isFolder)
+                        && imageValues != null && !TextUtils.isEmpty(url)) {
+                    int count = db.update(TABLE_IMAGES, imageValues, Images.URL + "=?",
+                            new String[] { url });
+                    if (count == 0) {
+                        db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues);
+                    }
+                }
+
+                id = db.insertOrThrow(TABLE_BOOKMARKS, Bookmarks.DIRTY, values);
+                refreshWidgets();
+                break;
+            }
+
+            case HISTORY: {
+                // If no created time is specified set it to now
+                if (!values.containsKey(History.DATE_CREATED)) {
+                    values.put(History.DATE_CREATED, System.currentTimeMillis());
+                }
+                String url = values.getAsString(History.URL);
+                url = filterSearchClient(url);
+                values.put(History.URL, url);
+
+                // Extract out the image values so they can be inserted into the images table
+                ContentValues imageValues = extractImageValues(values,
+                        values.getAsString(History.URL));
+                if (imageValues != null) {
+                    db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, imageValues);
+                }
+
+                id = db.insertOrThrow(TABLE_HISTORY, History.VISITS, values);
+                break;
+            }
+
+            case SEARCHES: {
+                id = insertSearchesInTransaction(db, values);
+                break;
+            }
+
+            case SYNCSTATE: {
+                id = mSyncHelper.insert(db, values);
+                break;
+            }
+
+            case SETTINGS: {
+                id = 0;
+                insertSettingsInTransaction(db, values);
+                break;
+            }
+
+            case THUMBNAILS: {
+                id = db.replaceOrThrow(TABLE_THUMBNAILS, null, values);
+                break;
+            }
+
+            default: {
+                throw new UnsupportedOperationException("Unknown insert URI " + uri);
+            }
+        }
+
+        if (id >= 0) {
+            postNotifyUri(uri);
+            if (shouldNotifyLegacy(uri)) {
+                postNotifyUri(LEGACY_AUTHORITY_URI);
+            }
+            return ContentUris.withAppendedId(uri, id);
+        } else {
+            return null;
+        }
+    }
+
+    private String[] getAccountNameAndType(long id) {
+        if (id <= 0) {
+            return null;
+        }
+        Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id);
+        Cursor c = query(uri,
+                new String[] { Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE },
+                null, null, null);
+        try {
+            if (c.moveToFirst()) {
+                String parentName = c.getString(0);
+                String parentType = c.getString(1);
+                return new String[] { parentName, parentType };
+            }
+            return null;
+        } finally {
+            c.close();
+        }
+    }
+
+    private boolean setParentValues(long parentId, ContentValues values) {
+        String[] parent = getAccountNameAndType(parentId);
+        if (parent == null) {
+            return false;
+        }
+        values.put(Bookmarks.ACCOUNT_NAME, parent[0]);
+        values.put(Bookmarks.ACCOUNT_TYPE, parent[1]);
+        return true;
+    }
+
+    private boolean isValidParent(String accountType, String accountName,
+            long parentId) {
+        String[] parent = getAccountNameAndType(parentId);
+        if (parent != null
+                && TextUtils.equals(accountName, parent[0])
+                && TextUtils.equals(accountType, parent[1])) {
+            return true;
+        }
+        return false;
+    }
+
+    private void filterSearchClient(String[] selectionArgs) {
+        if (selectionArgs != null) {
+            for (int i = 0; i < selectionArgs.length; i++) {
+                selectionArgs[i] = filterSearchClient(selectionArgs[i]);
+            }
+        }
+    }
+
+    // Filters out the client= param for search urls
+    private String filterSearchClient(String url) {
+        // remove "client" before updating it to the history so that it wont
+        // show up in the auto-complete list.
+        int index = url.indexOf("client=");
+        if (index > 0 && url.contains(".google.")) {
+            int end = url.indexOf('&', index);
+            if (end > 0) {
+                url = url.substring(0, index)
+                        .concat(url.substring(end + 1));
+            } else {
+                // the url.charAt(index-1) should be either '?' or '&'
+                url = url.substring(0, index-1);
+            }
+        }
+        return url;
+    }
+
+    /**
+     * Searches are unique, so perform an UPSERT manually since SQLite doesn't support them.
+     */
+    private long insertSearchesInTransaction(SQLiteDatabase db, ContentValues values) {
+        String search = values.getAsString(Searches.SEARCH);
+        if (TextUtils.isEmpty(search)) {
+            throw new IllegalArgumentException("Must include the SEARCH field");
+        }
+        Cursor cursor = null;
+        try {
+            cursor = db.query(TABLE_SEARCHES, new String[] { Searches._ID },
+                    Searches.SEARCH + "=?", new String[] { search }, null, null, null);
+            if (cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+                db.update(TABLE_SEARCHES, values, Searches._ID + "=?",
+                        new String[] { Long.toString(id) });
+                return id;
+            } else {
+                return db.insertOrThrow(TABLE_SEARCHES, Searches.SEARCH, values);
+            }
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+    }
+
+    /**
+     * Settings are unique, so perform an UPSERT manually since SQLite doesn't support them.
+     */
+    private long insertSettingsInTransaction(SQLiteDatabase db, ContentValues values) {
+        String key = values.getAsString(Settings.KEY);
+        if (TextUtils.isEmpty(key)) {
+            throw new IllegalArgumentException("Must include the KEY field");
+        }
+        String[] keyArray = new String[] { key };
+        Cursor cursor = null;
+        try {
+            cursor = db.query(TABLE_SETTINGS, new String[] { Settings.KEY },
+                    Settings.KEY + "=?", keyArray, null, null, null);
+            if (cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+                db.update(TABLE_SETTINGS, values, Settings.KEY + "=?", keyArray);
+                return id;
+            } else {
+                return db.insertOrThrow(TABLE_SETTINGS, Settings.VALUE, values);
+            }
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+    }
+
+    @Override
+    public int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs, boolean callerIsSyncAdapter) {
+        int match = URI_MATCHER.match(uri);
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        if (match == LEGACY || match == LEGACY_ID) {
+            // Intercept and route to the correct table
+            Integer bookmark = values.getAsInteger(BookmarkColumns.BOOKMARK);
+            values.remove(BookmarkColumns.BOOKMARK);
+            if (bookmark == null || bookmark == 0) {
+                if (match == LEGACY) {
+                    match = HISTORY;
+                } else {
+                    match = HISTORY_ID;
+                }
+            } else {
+                if (match == LEGACY) {
+                    match = BOOKMARKS;
+                } else {
+                    match = BOOKMARKS_ID;
+                }
+                values.remove(BookmarkColumns.DATE);
+                values.remove(BookmarkColumns.VISITS);
+                values.remove(BookmarkColumns.USER_ENTERED);
+            }
+        }
+        int modified = 0;
+        switch (match) {
+            case BOOKMARKS_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection,
+                        TABLE_BOOKMARKS + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case BOOKMARKS: {
+                Object[] withAccount = getSelectionWithAccounts(uri, selection, selectionArgs);
+                selection = (String) withAccount[0];
+                selectionArgs = (String[]) withAccount[1];
+                modified = updateBookmarksInTransaction(values, selection, selectionArgs,
+                        callerIsSyncAdapter);
+                if (modified > 0) {
+                    refreshWidgets();
+                }
+                break;
+            }
+
+            case HISTORY_ID: {
+                selection = DatabaseUtils.concatenateWhere(selection, TABLE_HISTORY + "._id=?");
+                selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                        new String[] { Long.toString(ContentUris.parseId(uri)) });
+                // fall through
+            }
+            case HISTORY: {
+                modified = updateHistoryInTransaction(values, selection, selectionArgs);
+                break;
+            }
+
+            case SYNCSTATE: {
+                modified = mSyncHelper.update(mDb, values,
+                        appendAccountToSelection(uri, selection), selectionArgs);
+                break;
+            }
+
+            case SYNCSTATE_ID: {
+                selection = appendAccountToSelection(uri, selection);
+                String selectionWithId =
+                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
+                        + (selection == null ? "" : " AND (" + selection + ")");
+                modified = mSyncHelper.update(mDb, values,
+                        selectionWithId, selectionArgs);
+                break;
+            }
+
+            case IMAGES: {
+                String url = values.getAsString(Images.URL);
+                if (TextUtils.isEmpty(url)) {
+                    throw new IllegalArgumentException("Images.URL is required");
+                }
+                if (!shouldUpdateImages(db, url, values)) {
+                    return 0;
+                }
+                int count = db.update(TABLE_IMAGES, values, Images.URL + "=?",
+                        new String[] { url });
+                if (count == 0) {
+                    db.insertOrThrow(TABLE_IMAGES, Images.FAVICON, values);
+                    count = 1;
+                }
+                // Only favicon is exposed in the public API. If we updated
+                // the thumbnail or touch icon don't bother notifying the
+                // legacy authority since it can't read it anyway.
+                boolean updatedLegacy = false;
+                if (getUrlCount(db, TABLE_BOOKMARKS, url) > 0) {
+                    postNotifyUri(Bookmarks.CONTENT_URI);
+                    updatedLegacy = values.containsKey(Images.FAVICON);
+                    refreshWidgets();
+                }
+                if (getUrlCount(db, TABLE_HISTORY, url) > 0) {
+                    postNotifyUri(History.CONTENT_URI);
+                    updatedLegacy = values.containsKey(Images.FAVICON);
+                }
+                if (pruneImages() > 0 || updatedLegacy) {
+                    postNotifyUri(LEGACY_AUTHORITY_URI);
+                }
+                // Even though we may be calling notifyUri on Bookmarks, don't
+                // sync to network as images aren't synced. Otherwise this
+                // unnecessarily triggers a bookmark sync.
+                mSyncToNetwork = false;
+                return count;
+            }
+
+            case SEARCHES: {
+                modified = db.update(TABLE_SEARCHES, values, selection, selectionArgs);
+                break;
+            }
+
+            case ACCOUNTS: {
+                Account[] accounts = AccountManager.get(getContext()).getAccounts();
+                mSyncHelper.onAccountsChanged(mDb, accounts);
+                break;
+            }
+
+            case THUMBNAILS: {
+                modified = db.update(TABLE_THUMBNAILS, values,
+                        selection, selectionArgs);
+                break;
+            }
+
+            case HOMEPAGE: {
+                if (null != values) {
+                    String homepage = values.getAsString("homepage");
+                    if (null != homepage) {
+                        if (BrowserSettings.getInstance() == null) {
+                            BrowserSettings.initialize(getContext());
+                        }
+                        BrowserSettings.getInstance().setHomePage(homepage);
+                        Log.d(TAG,"set home page for DM");
+                        return 1;
+                    }
+                }
+                return 0;
+            }
+
+            default: {
+                throw new UnsupportedOperationException("Unknown update URI " + uri);
+            }
+        }
+        pruneImages();
+        if (modified > 0) {
+            postNotifyUri(uri);
+            if (shouldNotifyLegacy(uri)) {
+                postNotifyUri(LEGACY_AUTHORITY_URI);
+            }
+        }
+        return modified;
+    }
+
+    // We want to avoid sending out more URI notifications than we have to
+    // Thus, we check to see if the images we are about to store are already there
+    // This is used because things like a site's favion or touch icon is rarely
+    // changed, but the browser tries to update it every time the page loads.
+    // Without this, we will always send out 3 URI notifications per page load.
+    // With this, that drops to 0 or 1, depending on if the thumbnail changed.
+    private boolean shouldUpdateImages(
+            SQLiteDatabase db, String url, ContentValues values) {
+        final String[] projection = new String[] {
+                Images.FAVICON,
+                Images.THUMBNAIL,
+                Images.TOUCH_ICON,
+        };
+        Cursor cursor = db.query(TABLE_IMAGES, projection, Images.URL + "=?",
+                new String[] { url }, null, null, null);
+        byte[] nfavicon = values.getAsByteArray(Images.FAVICON);
+        byte[] nthumb = values.getAsByteArray(Images.THUMBNAIL);
+        byte[] ntouch = values.getAsByteArray(Images.TOUCH_ICON);
+        byte[] cfavicon = null;
+        byte[] cthumb = null;
+        byte[] ctouch = null;
+        try {
+            if (cursor.getCount() <= 0) {
+                return nfavicon != null || nthumb != null || ntouch != null;
+            }
+            while (cursor.moveToNext()) {
+                if (nfavicon != null) {
+                    cfavicon = cursor.getBlob(0);
+                    if (!Arrays.equals(nfavicon, cfavicon)) {
+                        return true;
+                    }
+                }
+                if (nthumb != null) {
+                    cthumb = cursor.getBlob(1);
+                    if (!Arrays.equals(nthumb, cthumb)) {
+                        return true;
+                    }
+                }
+                if (ntouch != null) {
+                    ctouch = cursor.getBlob(2);
+                    if (!Arrays.equals(ntouch, ctouch)) {
+                        return true;
+                    }
+                }
+            }
+        } finally {
+            cursor.close();
+        }
+        return false;
+    }
+
+    int getUrlCount(SQLiteDatabase db, String table, String url) {
+        Cursor c = db.query(table, new String[] { "COUNT(*)" },
+                "url = ?", new String[] { url }, null, null, null);
+        try {
+            int count = 0;
+            if (c.moveToFirst()) {
+                count = c.getInt(0);
+            }
+            return count;
+        } finally {
+            c.close();
+        }
+    }
+
+    /**
+     * Does a query to find the matching bookmarks and updates each one with the provided values.
+     */
+    int updateBookmarksInTransaction(ContentValues values, String selection,
+            String[] selectionArgs, boolean callerIsSyncAdapter) {
+        int count = 0;
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        final String[] bookmarksProjection = new String[] {
+                Bookmarks._ID, // 0
+                Bookmarks.VERSION, // 1
+                Bookmarks.URL, // 2
+                Bookmarks.TITLE, // 3
+                Bookmarks.IS_FOLDER, // 4
+                Bookmarks.ACCOUNT_NAME, // 5
+                Bookmarks.ACCOUNT_TYPE, // 6
+        };
+        Cursor cursor = db.query(TABLE_BOOKMARKS, bookmarksProjection,
+                selection, selectionArgs, null, null, null);
+        boolean updatingParent = values.containsKey(Bookmarks.PARENT);
+        String parentAccountName = null;
+        String parentAccountType = null;
+        if (updatingParent) {
+            long parent = values.getAsLong(Bookmarks.PARENT);
+            Cursor c = db.query(TABLE_BOOKMARKS, new String[] {
+                    Bookmarks.ACCOUNT_NAME, Bookmarks.ACCOUNT_TYPE},
+                    "_id = ?", new String[] { Long.toString(parent) },
+                    null, null, null);
+            if (c.moveToFirst()) {
+                parentAccountName = c.getString(0);
+                parentAccountType = c.getString(1);
+            }
+            c.close();
+        } else if (values.containsKey(Bookmarks.ACCOUNT_NAME)
+                || values.containsKey(Bookmarks.ACCOUNT_TYPE)) {
+            // TODO: Implement if needed (no one needs this yet)
+        }
+        try {
+            String[] args = new String[1];
+            // Mark the bookmark dirty if the caller isn't a sync adapter
+            if (!callerIsSyncAdapter) {
+                values.put(Bookmarks.DATE_MODIFIED, System.currentTimeMillis());
+                values.put(Bookmarks.DIRTY, 1);
+            }
+
+            boolean updatingUrl = values.containsKey(Bookmarks.URL);
+            String url = null;
+            if (updatingUrl) {
+                url = values.getAsString(Bookmarks.URL);
+            }
+            ContentValues imageValues = extractImageValues(values, url);
+
+            while (cursor.moveToNext()) {
+                long id = cursor.getLong(0);
+                args[0] = Long.toString(id);
+                String accountName = cursor.getString(5);
+                String accountType = cursor.getString(6);
+                // If we are updating the parent and either the account name or
+                // type do not match that of the new parent
+                if (updatingParent
+                        && (!TextUtils.equals(accountName, parentAccountName)
+                        || !TextUtils.equals(accountType, parentAccountType))) {
+                    // Parent is a different account
+                    // First, insert a new bookmark/folder with the new account
+                    // Then, if this is a folder, reparent all it's children
+                    // Finally, delete the old bookmark/folder
+                    ContentValues newValues = valuesFromCursor(cursor);
+                    newValues.putAll(values);
+                    newValues.remove(Bookmarks._ID);
+                    newValues.remove(Bookmarks.VERSION);
+                    newValues.put(Bookmarks.ACCOUNT_NAME, parentAccountName);
+                    newValues.put(Bookmarks.ACCOUNT_TYPE, parentAccountType);
+                    Uri insertUri = insertInTransaction(Bookmarks.CONTENT_URI,
+                            newValues, callerIsSyncAdapter);
+                    long newId = ContentUris.parseId(insertUri);
+                    if (cursor.getInt(4) != 0) {
+                        // This is a folder, reparent
+                        ContentValues updateChildren = new ContentValues(1);
+                        updateChildren.put(Bookmarks.PARENT, newId);
+                        count += updateBookmarksInTransaction(updateChildren,
+                                Bookmarks.PARENT + "=?", new String[] {
+                                Long.toString(id)}, callerIsSyncAdapter);
+                    }
+                    // Now, delete the old one
+                    Uri uri = ContentUris.withAppendedId(Bookmarks.CONTENT_URI, id);
+                    deleteInTransaction(uri, null, null, callerIsSyncAdapter);
+                    count += 1;
+                } else {
+                    if (!callerIsSyncAdapter) {
+                        // increase the local version for non-sync changes
+                        values.put(Bookmarks.VERSION, cursor.getLong(1) + 1);
+                    }
+                    count += db.update(TABLE_BOOKMARKS, values, "_id=?", args);
+                }
+
+                // Update the images over in their table
+                if (imageValues != null) {
+                    if (!updatingUrl) {
+                        url = cursor.getString(2);
+                        imageValues.put(Images.URL, url);
+                    }
+
+                    if (!TextUtils.isEmpty(url)) {
+                        args[0] = url;
+                        if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) {
+                            db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
+                        }
+                    }
+                }
+            }
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+        return count;
+    }
+
+    ContentValues valuesFromCursor(Cursor c) {
+        int count = c.getColumnCount();
+        ContentValues values = new ContentValues(count);
+        String[] colNames = c.getColumnNames();
+        for (int i = 0; i < count; i++) {
+            switch (c.getType(i)) {
+            case Cursor.FIELD_TYPE_BLOB:
+                values.put(colNames[i], c.getBlob(i));
+                break;
+            case Cursor.FIELD_TYPE_FLOAT:
+                values.put(colNames[i], c.getFloat(i));
+                break;
+            case Cursor.FIELD_TYPE_INTEGER:
+                values.put(colNames[i], c.getLong(i));
+                break;
+            case Cursor.FIELD_TYPE_STRING:
+                values.put(colNames[i], c.getString(i));
+                break;
+            }
+        }
+        return values;
+    }
+
+    /**
+     * Does a query to find the matching bookmarks and updates each one with the provided values.
+     */
+    int updateHistoryInTransaction(ContentValues values, String selection, String[] selectionArgs) {
+        int count = 0;
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        filterSearchClient(selectionArgs);
+        Cursor cursor = query(History.CONTENT_URI,
+                new String[] { History._ID, History.URL },
+                selection, selectionArgs, null);
+        try {
+            String[] args = new String[1];
+
+            boolean updatingUrl = values.containsKey(History.URL);
+            String url = null;
+            if (updatingUrl) {
+                url = filterSearchClient(values.getAsString(History.URL));
+                values.put(History.URL, url);
+            }
+            ContentValues imageValues = extractImageValues(values, url);
+
+            while (cursor.moveToNext()) {
+                args[0] = cursor.getString(0);
+                count += db.update(TABLE_HISTORY, values, "_id=?", args);
+
+                // Update the images over in their table
+                if (imageValues != null) {
+                    if (!updatingUrl) {
+                        url = cursor.getString(1);
+                        imageValues.put(Images.URL, url);
+                    }
+                    args[0] = url;
+                    if (db.update(TABLE_IMAGES, imageValues, Images.URL + "=?", args) == 0) {
+                        db.insert(TABLE_IMAGES, Images.FAVICON, imageValues);
+                    }
+                }
+            }
+        } finally {
+            if (cursor != null) cursor.close();
+        }
+        return count;
+    }
+
+    String appendAccountToSelection(Uri uri, String selection) {
+        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
+        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
+
+        final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
+        if (partialUri) {
+            // Throw when either account is incomplete
+            throw new IllegalArgumentException(
+                    "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE for " + uri);
+        }
+
+        // Accounts are valid by only checking one parameter, since we've
+        // already ruled out partial accounts.
+        final boolean validAccount = !TextUtils.isEmpty(accountName);
+        if (validAccount) {
+            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
+                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
+                    + RawContacts.ACCOUNT_TYPE + "="
+                    + DatabaseUtils.sqlEscapeString(accountType));
+            if (!TextUtils.isEmpty(selection)) {
+                selectionSb.append(" AND (");
+                selectionSb.append(selection);
+                selectionSb.append(')');
+            }
+            return selectionSb.toString();
+        } else {
+            return selection;
+        }
+    }
+
+    ContentValues extractImageValues(ContentValues values, String url) {
+        ContentValues imageValues = null;
+        // favicon
+        if (values.containsKey(Bookmarks.FAVICON)) {
+            imageValues = new ContentValues();
+            imageValues.put(Images.FAVICON, values.getAsByteArray(Bookmarks.FAVICON));
+            values.remove(Bookmarks.FAVICON);
+        }
+
+        // thumbnail
+        if (values.containsKey(Bookmarks.THUMBNAIL)) {
+            if (imageValues == null) {
+                imageValues = new ContentValues();
+            }
+            imageValues.put(Images.THUMBNAIL, values.getAsByteArray(Bookmarks.THUMBNAIL));
+            values.remove(Bookmarks.THUMBNAIL);
+        }
+
+        // touch icon
+        if (values.containsKey(Bookmarks.TOUCH_ICON)) {
+            if (imageValues == null) {
+                imageValues = new ContentValues();
+            }
+            imageValues.put(Images.TOUCH_ICON, values.getAsByteArray(Bookmarks.TOUCH_ICON));
+            values.remove(Bookmarks.TOUCH_ICON);
+        }
+
+        if (imageValues != null) {
+            imageValues.put(Images.URL,  url);
+        }
+        return imageValues;
+    }
+
+    int pruneImages() {
+        final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        return db.delete(TABLE_IMAGES, IMAGE_PRUNE, null);
+    }
+
+    boolean shouldNotifyLegacy(Uri uri) {
+        if (uri.getPathSegments().contains("history")
+                || uri.getPathSegments().contains("bookmarks")
+                || uri.getPathSegments().contains("searches")) {
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean syncToNetwork(Uri uri) {
+        if (BrowserContract.AUTHORITY.equals(uri.getAuthority())
+                && uri.getPathSegments().contains("bookmarks")) {
+            return mSyncToNetwork;
+        }
+        if (LEGACY_AUTHORITY.equals(uri.getAuthority())) {
+            // Allow for 3rd party sync adapters
+            return true;
+        }
+        return false;
+    }
+
+    static class SuggestionsCursor extends AbstractCursor {
+        private static final int ID_INDEX = 0;
+        private static final int URL_INDEX = 1;
+        private static final int TITLE_INDEX = 2;
+        private static final int ICON_INDEX = 3;
+        private static final int LAST_ACCESS_TIME_INDEX = 4;
+        // shared suggestion array index, make sure to match COLUMNS
+        private static final int SUGGEST_COLUMN_INTENT_ACTION_ID = 1;
+        private static final int SUGGEST_COLUMN_INTENT_DATA_ID = 2;
+        private static final int SUGGEST_COLUMN_TEXT_1_ID = 3;
+        private static final int SUGGEST_COLUMN_TEXT_2_TEXT_ID = 4;
+        private static final int SUGGEST_COLUMN_TEXT_2_URL_ID = 5;
+        private static final int SUGGEST_COLUMN_ICON_1_ID = 6;
+        private static final int SUGGEST_COLUMN_LAST_ACCESS_HINT_ID = 7;
+
+        // shared suggestion columns
+        private static final String[] COLUMNS = new String[] {
+                BaseColumns._ID,
+                SearchManager.SUGGEST_COLUMN_INTENT_ACTION,
+                SearchManager.SUGGEST_COLUMN_INTENT_DATA,
+                SearchManager.SUGGEST_COLUMN_TEXT_1,
+                SearchManager.SUGGEST_COLUMN_TEXT_2,
+                SearchManager.SUGGEST_COLUMN_TEXT_2_URL,
+                SearchManager.SUGGEST_COLUMN_ICON_1,
+                SearchManager.SUGGEST_COLUMN_LAST_ACCESS_HINT};
+
+        private final Cursor mSource;
+
+        public SuggestionsCursor(Cursor cursor) {
+            mSource = cursor;
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            return COLUMNS;
+        }
+
+        @Override
+        public String getString(int columnIndex) {
+            switch (columnIndex) {
+            case ID_INDEX:
+                return mSource.getString(columnIndex);
+            case SUGGEST_COLUMN_INTENT_ACTION_ID:
+                return Intent.ACTION_VIEW;
+            case SUGGEST_COLUMN_INTENT_DATA_ID:
+                return mSource.getString(URL_INDEX);
+            case SUGGEST_COLUMN_TEXT_2_TEXT_ID:
+            case SUGGEST_COLUMN_TEXT_2_URL_ID:
+                return UrlUtils.stripUrl(mSource.getString(URL_INDEX));
+            case SUGGEST_COLUMN_TEXT_1_ID:
+                return mSource.getString(TITLE_INDEX);
+            case SUGGEST_COLUMN_ICON_1_ID:
+                return mSource.getString(ICON_INDEX);
+            case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID:
+                return mSource.getString(LAST_ACCESS_TIME_INDEX);
+            }
+            return null;
+        }
+
+        @Override
+        public int getCount() {
+            return mSource.getCount();
+        }
+
+        @Override
+        public double getDouble(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public float getFloat(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getInt(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getLong(int column) {
+            switch (column) {
+            case ID_INDEX:
+                return mSource.getLong(ID_INDEX);
+            case SUGGEST_COLUMN_LAST_ACCESS_HINT_ID:
+                return mSource.getLong(LAST_ACCESS_TIME_INDEX);
+            }
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public short getShort(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isNull(int column) {
+            return mSource.isNull(column);
+        }
+
+        @Override
+        public boolean onMove(int oldPosition, int newPosition) {
+            return mSource.moveToPosition(newPosition);
+        }
+    }
+
+    // ---------------------------------------------------
+    //  SQL below, be warned
+    // ---------------------------------------------------
+
+    private static final String SQL_CREATE_VIEW_OMNIBOX_SUGGESTIONS =
+            "CREATE VIEW IF NOT EXISTS v_omnibox_suggestions "
+            + " AS "
+            + "  SELECT _id, url, title, 1 AS bookmark, 0 AS visits, 0 AS date"
+            + "  FROM bookmarks "
+            + "  WHERE deleted = 0 AND folder = 0 "
+            + "  UNION ALL "
+            + "  SELECT _id, url, title, 0 AS bookmark, visits, date "
+            + "  FROM history "
+            + "  WHERE url NOT IN (SELECT url FROM bookmarks"
+            + "    WHERE deleted = 0 AND folder = 0) "
+            + "  ORDER BY bookmark DESC, visits DESC, date DESC ";
+
+    private static final String SQL_WHERE_ACCOUNT_HAS_BOOKMARKS =
+            "0 < ( "
+            + "SELECT count(*) "
+            + "FROM bookmarks "
+            + "WHERE deleted = 0 AND folder = 0 "
+            + "  AND ( "
+            + "    v_accounts.account_name = bookmarks.account_name "
+            + "    OR (v_accounts.account_name IS NULL AND bookmarks.account_name IS NULL) "
+            + "  ) "
+            + "  AND ( "
+            + "    v_accounts.account_type = bookmarks.account_type "
+            + "    OR (v_accounts.account_type IS NULL AND bookmarks.account_type IS NULL) "
+            + "  ) "
+            + ")";
+}
diff --git a/src/com/android/browser/provider/MyNavigationProvider.java b/src/com/android/browser/provider/MyNavigationProvider.java
new file mode 100755
index 0000000..4cd3391
--- /dev/null
+++ b/src/com/android/browser/provider/MyNavigationProvider.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (c) 2013, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *     * Redistributions of source code must retain the above copyright
+ *       notice, this list of conditions and the following disclaimer.
+ *       * Redistributions in binary form must reproduce the above
+ *         copyright notice, this list of conditions and the following
+ *         disclaimer in the documentation and/or other materials provided
+ *         with the distribution.
+ *       * Neither the name of The Linux Foundation nor the names of its
+ *         contributors may be used to endorse or promote products derived
+ *         from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package com.android.browser.provider;
+
+import android.content.Context;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.UriMatcher;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.os.ParcelFileDescriptor;
+import android.text.TextUtils;
+import android.util.Log;
+import android.webkit.WebResourceResponse;
+
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+
+import com.android.browser.BrowserSettings;
+import com.android.browser.R;
+import com.android.browser.homepages.RequestHandler;
+import com.android.browser.mynavigation.MyNavigationRequestHandler;
+import com.android.browser.mynavigation.MyNavigationUtil;
+import com.android.browser.provider.BrowserProvider2;
+
+public class MyNavigationProvider extends ContentProvider {
+
+    private static final String LOGTAG = "MyNavigationProvider";
+    private static final String TABLE_WEB_SITES = "websites";
+    private static final int WEB_SITES_ALL = 0;
+    private static final int WEB_SITES_ID = 1;
+    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+    static {
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites", WEB_SITES_ALL);
+        URI_MATCHER.addURI(MyNavigationUtil.AUTHORITY, "websites/#", WEB_SITES_ID);
+    }
+    private static final Uri NOTIFICATION_URI = MyNavigationUtil.MY_NAVIGATION_URI;
+
+    private SiteNavigationDatabaseHelper mOpenHelper;
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        // Current not used, just return 0
+        return 0;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        // Current not used, just return null
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        // Current not used, just return null
+        return null;
+    }
+
+    @Override
+    public boolean onCreate() {
+        mOpenHelper = new SiteNavigationDatabaseHelper(this.getContext());
+        return true;
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        qb.setTables(TABLE_WEB_SITES);
+        switch (URI_MATCHER.match(uri)) {
+            case WEB_SITES_ALL:
+                break;
+            case WEB_SITES_ID:
+                qb.appendWhere(MyNavigationUtil.ID + "=" + uri.getPathSegments().get(0));
+                break;
+            default:
+                Log.e(LOGTAG, "query Unknown URI: " + uri);
+                return null;
+        }
+
+        String orderBy;
+        if (TextUtils.isEmpty(sortOrder)) {
+            orderBy = null;
+        } else {
+            orderBy = sortOrder;
+        }
+        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
+        Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy);
+        if (c != null) {
+            c.setNotificationUri(getContext().getContentResolver(), NOTIFICATION_URI);
+        }
+        return c;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
+        int count = 0;
+
+        switch (URI_MATCHER.match(uri)) {
+            case WEB_SITES_ALL:
+                count = db.update(TABLE_WEB_SITES, values, selection, selectionArgs);
+                break;
+            case WEB_SITES_ID:
+                String newIdSelection = MyNavigationUtil.ID + "=" + uri.getLastPathSegment()
+                        + (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : "");
+                count = db.update(TABLE_WEB_SITES, values, newIdSelection, selectionArgs);
+                break;
+            default:
+                Log.e(LOGTAG, "update Unknown URI: " + uri);
+                return count;
+        }
+
+        if (count > 0) {
+            ContentResolver cr = getContext().getContentResolver();
+            cr.notifyChange(uri, null);
+        }
+        return count;
+    }
+
+    @Override
+    public ParcelFileDescriptor openFile(Uri uri, String mode) {
+        try {
+            ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe();
+            final ParcelFileDescriptor write = pipes[1];
+            AssetFileDescriptor afd = new AssetFileDescriptor(write, 0, -1);
+            new MyNavigationRequestHandler(getContext(), uri, afd.createOutputStream())
+                    .start();
+            return pipes[0];
+        } catch (IOException e) {
+            Log.e(LOGTAG, "Failed to handle request: " + uri, e);
+            return null;
+        }
+    }
+
+    public static WebResourceResponse shouldInterceptRequest(Context context,
+            String url) {
+        try {
+            if (MyNavigationUtil.MY_NAVIGATION.equals(url)) {
+                Uri uri = Uri.parse(url);
+                if (MyNavigationUtil.AUTHORITY.equals(uri.getAuthority())) {
+                    InputStream ins = context.getContentResolver()
+                            .openInputStream(uri);
+                    return new WebResourceResponse("text/html", "utf-8", ins);
+                }
+            }
+            boolean listFiles = BrowserSettings.getInstance().isDebugEnabled();
+            if (listFiles && interceptFile(url)) {
+                PipedInputStream ins = new PipedInputStream();
+                PipedOutputStream outs = new PipedOutputStream(ins);
+                new RequestHandler(context, Uri.parse(url), outs).start();
+                return new WebResourceResponse("text/html", "utf-8", ins);
+            }
+        } catch (Exception e) {}
+        return null;
+    }
+
+    private static boolean interceptFile(String url) {
+        if (!url.startsWith("file:///")) {
+            return false;
+        }
+        String fpath = url.substring(7);
+        File f = new File(fpath);
+        if (!f.isDirectory()) {
+            return false;
+        }
+        return true;
+    }
+
+    private class SiteNavigationDatabaseHelper extends SQLiteOpenHelper {
+        private Context mContext;
+        static final String DATABASE_NAME = "mynavigation.db";
+
+        public SiteNavigationDatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, 1); // "1" is the db version here
+            // TODO Auto-generated constructor stub
+            mContext = context;
+        }
+
+        public SiteNavigationDatabaseHelper(Context context, String name,
+                CursorFactory factory, int version) {
+            super(context, name, factory, version);
+            // TODO Auto-generated constructor stub
+            mContext = context;
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            // TODO Auto-generated method stub
+            createWebsitesTable(db);
+            initWebsitesTable(db);
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase arg0, int arg1, int arg2) {
+            // TODO Auto-generated method stub
+        }
+
+        private void createWebsitesTable(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE websites (" +
+                    MyNavigationUtil.ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    MyNavigationUtil.URL + " TEXT," +
+                    MyNavigationUtil.TITLE + " TEXT," +
+                    MyNavigationUtil.DATE_CREATED + " LONG," +
+                    MyNavigationUtil.WEBSITE + " INTEGER," +
+                    MyNavigationUtil.THUMBNAIL + " BLOB DEFAULT NULL," +
+                    MyNavigationUtil.FAVICON + " BLOB DEFAULT NULL," +
+                    MyNavigationUtil.DEFAULT_THUMB + " TEXT" +
+                    ");");
+        }
+
+        // initial table , insert websites to table websites
+        private void initWebsitesTable(SQLiteDatabase db) {
+            int WebsiteNumber = MyNavigationUtil.WEBSITE_NUMBER;
+            for (int i = 0; i < WebsiteNumber; i++) {
+                ByteArrayOutputStream os = new ByteArrayOutputStream();
+                Bitmap bm = BitmapFactory.decodeResource(mContext.getResources(),
+                        R.raw.my_navigation_add);
+                bm.compress(Bitmap.CompressFormat.PNG, 100, os);
+                ContentValues values = new ContentValues();
+                values.put(MyNavigationUtil.URL, "ae://" + (i + 1) + "add-fav");
+                values.put(MyNavigationUtil.TITLE, mContext.getString(R.string.my_navigation_add));
+                values.put(MyNavigationUtil.DATE_CREATED, 0 + "");
+                values.put(MyNavigationUtil.WEBSITE, 1 + "");
+                values.put(MyNavigationUtil.THUMBNAIL, os.toByteArray());
+                db.insertOrThrow(TABLE_WEB_SITES, MyNavigationUtil.URL, values);
+            }
+        }
+    }
+}
diff --git a/src/com/android/browser/provider/SQLiteContentProvider.java b/src/com/android/browser/provider/SQLiteContentProvider.java
new file mode 100644
index 0000000..75e298e
--- /dev/null
+++ b/src/com/android/browser/provider/SQLiteContentProvider.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2009 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderOperation;
+import android.content.ContentProviderResult;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.net.Uri;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
+ */
+public abstract class SQLiteContentProvider extends ContentProvider {
+
+    private static final String TAG = "SQLiteContentProvider";
+
+    private SQLiteOpenHelper mOpenHelper;
+    private Set<Uri> mChangedUris;
+    protected SQLiteDatabase mDb;
+
+    private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
+    private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
+
+    /**
+     * Maximum number of operations allowed in a batch between yield points.
+     */
+    private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
+
+    @Override
+    public boolean onCreate() {
+        Context context = getContext();
+        mOpenHelper = getDatabaseHelper(context);
+        mChangedUris = new HashSet<Uri>();
+        return true;
+    }
+
+    /**
+     * Returns a {@link SQLiteOpenHelper} that can open the database.
+     */
+    public abstract SQLiteOpenHelper getDatabaseHelper(Context context);
+
+    /**
+     * The equivalent of the {@link #insert} method, but invoked within a transaction.
+     */
+    public abstract Uri insertInTransaction(Uri uri, ContentValues values,
+            boolean callerIsSyncAdapter);
+
+    /**
+     * The equivalent of the {@link #update} method, but invoked within a transaction.
+     */
+    public abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs, boolean callerIsSyncAdapter);
+
+    /**
+     * The equivalent of the {@link #delete} method, but invoked within a transaction.
+     */
+    public abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs,
+            boolean callerIsSyncAdapter);
+
+    /**
+     * Call this to add a URI to the list of URIs to be notified when the transaction
+     * is committed.
+     */
+    protected void postNotifyUri(Uri uri) {
+        synchronized (mChangedUris) {
+            mChangedUris.add(uri);
+        }
+    }
+
+    public boolean isCallerSyncAdapter(Uri uri) {
+        return false;
+    }
+
+    public SQLiteOpenHelper getDatabaseHelper() {
+        return mOpenHelper;
+    }
+
+    private boolean applyingBatch() {
+        return mApplyingBatch.get() != null && mApplyingBatch.get();
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        Uri result = null;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            mDb = mOpenHelper.getWritableDatabase();
+            mDb.beginTransaction();
+            try {
+                result = insertInTransaction(uri, values, callerIsSyncAdapter);
+                mDb.setTransactionSuccessful();
+            } finally {
+                mDb.endTransaction();
+            }
+
+            onEndTransaction(callerIsSyncAdapter);
+        } else {
+            result = insertInTransaction(uri, values, callerIsSyncAdapter);
+        }
+        return result;
+    }
+
+    @Override
+    public int bulkInsert(Uri uri, ContentValues[] values) {
+        int numValues = values.length;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        mDb = mOpenHelper.getWritableDatabase();
+        mDb.beginTransaction();
+        try {
+            for (int i = 0; i < numValues; i++) {
+                Uri result = insertInTransaction(uri, values[i], callerIsSyncAdapter);
+                mDb.yieldIfContendedSafely();
+            }
+            mDb.setTransactionSuccessful();
+        } finally {
+            mDb.endTransaction();
+        }
+
+        onEndTransaction(callerIsSyncAdapter);
+        return numValues;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+        int count = 0;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            mDb = mOpenHelper.getWritableDatabase();
+            mDb.beginTransaction();
+            try {
+                count = updateInTransaction(uri, values, selection, selectionArgs,
+                        callerIsSyncAdapter);
+                mDb.setTransactionSuccessful();
+            } finally {
+                mDb.endTransaction();
+            }
+
+            onEndTransaction(callerIsSyncAdapter);
+        } else {
+            count = updateInTransaction(uri, values, selection, selectionArgs, callerIsSyncAdapter);
+        }
+
+        return count;
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        int count = 0;
+        boolean callerIsSyncAdapter = isCallerSyncAdapter(uri);
+        boolean applyingBatch = applyingBatch();
+        if (!applyingBatch) {
+            mDb = mOpenHelper.getWritableDatabase();
+            mDb.beginTransaction();
+            try {
+                count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+                mDb.setTransactionSuccessful();
+            } finally {
+                mDb.endTransaction();
+            }
+
+            onEndTransaction(callerIsSyncAdapter);
+        } else {
+            count = deleteInTransaction(uri, selection, selectionArgs, callerIsSyncAdapter);
+        }
+        return count;
+    }
+
+    @Override
+    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
+            throws OperationApplicationException {
+        int ypCount = 0;
+        int opCount = 0;
+        boolean callerIsSyncAdapter = false;
+        mDb = mOpenHelper.getWritableDatabase();
+        mDb.beginTransaction();
+        try {
+            mApplyingBatch.set(true);
+            final int numOperations = operations.size();
+            final ContentProviderResult[] results = new ContentProviderResult[numOperations];
+            for (int i = 0; i < numOperations; i++) {
+                if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
+                    throw new OperationApplicationException(
+                            "Too many content provider operations between yield points. "
+                                    + "The maximum number of operations per yield point is "
+                                    + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
+                }
+                final ContentProviderOperation operation = operations.get(i);
+                if (!callerIsSyncAdapter && isCallerSyncAdapter(operation.getUri())) {
+                    callerIsSyncAdapter = true;
+                }
+                if (i > 0 && operation.isYieldAllowed()) {
+                    opCount = 0;
+                    if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
+                        ypCount++;
+                    }
+                }
+                results[i] = operation.apply(this, results, i);
+            }
+            mDb.setTransactionSuccessful();
+            return results;
+        } finally {
+            mApplyingBatch.set(false);
+            mDb.endTransaction();
+            onEndTransaction(callerIsSyncAdapter);
+        }
+    }
+
+    protected void onEndTransaction(boolean callerIsSyncAdapter) {
+        Set<Uri> changed;
+        synchronized (mChangedUris) {
+            changed = new HashSet<Uri>(mChangedUris);
+            mChangedUris.clear();
+        }
+        ContentResolver resolver = getContext().getContentResolver();
+        for (Uri uri : changed) {
+            boolean syncToNetwork = !callerIsSyncAdapter && syncToNetwork(uri);
+            resolver.notifyChange(uri, null, syncToNetwork);
+        }
+    }
+
+    protected boolean syncToNetwork(Uri uri) {
+        return false;
+    }
+}
diff --git a/src/com/android/browser/provider/SnapshotProvider.java b/src/com/android/browser/provider/SnapshotProvider.java
new file mode 100644
index 0000000..3226c11
--- /dev/null
+++ b/src/com/android/browser/provider/SnapshotProvider.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2011 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.provider;
+
+import android.content.ContentProvider;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.DatabaseUtils;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteQueryBuilder;
+import android.net.Uri;
+
+import com.android.browser.platformsupport.BrowserContract;
+
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.FileOutputStream;
+import java.io.FileInputStream;
+
+public class SnapshotProvider extends ContentProvider {
+
+    public static interface Snapshots {
+
+        public static final Uri CONTENT_URI = Uri.withAppendedPath(
+                SnapshotProvider.AUTHORITY_URI, "snapshots");
+        public static final String _ID = "_id";
+        @Deprecated
+        public static final String VIEWSTATE = "view_state";
+        public static final String BACKGROUND = "background";
+        public static final String TITLE = "title";
+        public static final String URL = "url";
+        public static final String FAVICON = "favicon";
+        public static final String THUMBNAIL = "thumbnail";
+        public static final String DATE_CREATED = "date_created";
+        public static final String VIEWSTATE_PATH = "viewstate_path";
+        public static final String VIEWSTATE_SIZE = "viewstate_size";
+    }
+
+    public static final String AUTHORITY = "com.android.browser.snapshots";
+    public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+
+    static final String TABLE_SNAPSHOTS = "snapshots";
+    static final int SNAPSHOTS = 10;
+    static final int SNAPSHOTS_ID = 11;
+    static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
+    // Workaround that we can't remove the "NOT NULL" constraint on VIEWSTATE
+    static final byte[] NULL_BLOB_HACK = new byte[0];
+
+    SnapshotDatabaseHelper mOpenHelper;
+
+    static {
+        URI_MATCHER.addURI(AUTHORITY, "snapshots", SNAPSHOTS);
+        URI_MATCHER.addURI(AUTHORITY, "snapshots/#", SNAPSHOTS_ID);
+    }
+
+    final static class SnapshotDatabaseHelper extends SQLiteOpenHelper {
+
+        static final String DATABASE_NAME = "snapshots.db";
+        static final int DATABASE_VERSION = 3;
+
+        public SnapshotDatabaseHelper(Context context) {
+            super(context, DATABASE_NAME, null, DATABASE_VERSION);
+        }
+
+        @Override
+        public void onCreate(SQLiteDatabase db) {
+            db.execSQL("CREATE TABLE " + TABLE_SNAPSHOTS + "(" +
+                    Snapshots._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+                    Snapshots.TITLE + " TEXT," +
+                    Snapshots.URL + " TEXT NOT NULL," +
+                    Snapshots.DATE_CREATED + " INTEGER," +
+                    Snapshots.FAVICON + " BLOB," +
+                    Snapshots.THUMBNAIL + " BLOB," +
+                    Snapshots.BACKGROUND + " INTEGER," +
+                    Snapshots.VIEWSTATE + " BLOB NOT NULL," +
+                    Snapshots.VIEWSTATE_PATH + " TEXT," +
+                    Snapshots.VIEWSTATE_SIZE + " INTEGER" +
+                    ");");
+        }
+
+        @Override
+        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+            if (oldVersion < 2) {
+                db.execSQL("DROP TABLE " + TABLE_SNAPSHOTS);
+                onCreate(db);
+            }
+            if (oldVersion < 3) {
+                db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
+                        + Snapshots.VIEWSTATE_PATH + " TEXT");
+                db.execSQL("ALTER TABLE " + TABLE_SNAPSHOTS + " ADD COLUMN "
+                        + Snapshots.VIEWSTATE_SIZE + " INTEGER");
+                db.execSQL("UPDATE " + TABLE_SNAPSHOTS + " SET "
+                        + Snapshots.VIEWSTATE_SIZE + " = length("
+                        + Snapshots.VIEWSTATE + ")");
+            }
+        }
+
+    }
+
+    static File getOldDatabasePath(Context context) {
+        File dir = context.getExternalFilesDir(null);
+        return new File(dir, SnapshotDatabaseHelper.DATABASE_NAME);
+    }
+
+    private static boolean copyFile(File srcFile, File destFile) {
+        try {
+            if (destFile.exists()) {
+                destFile.delete();
+            }
+
+            FileInputStream in = new FileInputStream(srcFile);
+            FileOutputStream out = new FileOutputStream(destFile);
+
+            try {
+                byte[] buffer = new byte[4096];
+                int bytesRead;
+                while ((bytesRead = in.read(buffer)) >= 0) {
+                    out.write(buffer, 0, bytesRead);
+                }
+            } finally {
+                out.flush();
+                try {
+                    out.getFD().sync();
+                } catch (IOException e) {
+                }
+                in.close();
+                out.close();
+            }
+            return true;
+        } catch (IOException e) {
+            return false;
+        }
+    }
+
+    private void migrateToDataFolder() {
+        File dbPath = getContext().getDatabasePath(SnapshotDatabaseHelper.DATABASE_NAME);
+        if (dbPath.exists()) return;
+        File oldPath = getOldDatabasePath(getContext());
+        if (oldPath.exists()) {
+            // Try to move
+            if (!oldPath.renameTo(dbPath)) {
+                // Failed, do a copy
+                copyFile(oldPath, dbPath);
+            }
+            // Cleanup
+            oldPath.delete();
+        }
+    }
+
+    @Override
+    public boolean onCreate() {
+        migrateToDataFolder();
+        mOpenHelper = new SnapshotDatabaseHelper(getContext());
+        return true;
+    }
+
+    SQLiteDatabase getWritableDatabase() {
+        return mOpenHelper.getWritableDatabase();
+    }
+
+    SQLiteDatabase getReadableDatabase() {
+        return mOpenHelper.getReadableDatabase();
+    }
+
+    @Override
+    public Cursor query(Uri uri, String[] projection, String selection,
+            String[] selectionArgs, String sortOrder) {
+        SQLiteDatabase db = getReadableDatabase();
+        if (db == null) {
+            return null;
+        }
+        final int match = URI_MATCHER.match(uri);
+        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
+        String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
+        switch (match) {
+        case SNAPSHOTS_ID:
+            selection = DatabaseUtils.concatenateWhere(selection, "_id=?");
+            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                    new String[] { Long.toString(ContentUris.parseId(uri)) });
+            // fall through
+        case SNAPSHOTS:
+            qb.setTables(TABLE_SNAPSHOTS);
+            break;
+
+        default:
+            throw new UnsupportedOperationException("Unknown URL " + uri.toString());
+        }
+        Cursor cursor = qb.query(db, projection, selection, selectionArgs,
+                null, null, sortOrder, limit);
+        cursor.setNotificationUri(getContext().getContentResolver(),
+                AUTHORITY_URI);
+        return cursor;
+    }
+
+    @Override
+    public String getType(Uri uri) {
+        return null;
+    }
+
+    @Override
+    public Uri insert(Uri uri, ContentValues values) {
+        SQLiteDatabase db = getWritableDatabase();
+        if (db == null) {
+            return null;
+        }
+        int match = URI_MATCHER.match(uri);
+        long id = -1;
+        switch (match) {
+        case SNAPSHOTS:
+            if (!values.containsKey(Snapshots.VIEWSTATE)) {
+                values.put(Snapshots.VIEWSTATE, NULL_BLOB_HACK);
+            }
+            id = db.insert(TABLE_SNAPSHOTS, Snapshots.TITLE, values);
+            break;
+        default:
+            throw new UnsupportedOperationException("Unknown insert URI " + uri);
+        }
+        if (id < 0) {
+            return null;
+        }
+        Uri inserted = ContentUris.withAppendedId(uri, id);
+        getContext().getContentResolver().notifyChange(inserted, null, false);
+        return inserted;
+    }
+
+    static final String[] DELETE_PROJECTION = new String[] {
+        Snapshots.VIEWSTATE_PATH,
+    };
+    private void deleteDataFiles(SQLiteDatabase db, String selection,
+            String[] selectionArgs) {
+        Cursor c = db.query(TABLE_SNAPSHOTS, DELETE_PROJECTION, selection,
+                selectionArgs, null, null, null);
+        final Context context = getContext();
+        while (c.moveToNext()) {
+            String filename = c.getString(0);
+            if (TextUtils.isEmpty(filename)) {
+                continue;
+            }
+            File f = context.getFileStreamPath(filename);
+            if (f.exists()) {
+                if (!f.delete()) {
+                    f.deleteOnExit();
+                }
+            }
+        }
+        c.close();
+    }
+
+    @Override
+    public int delete(Uri uri, String selection, String[] selectionArgs) {
+        SQLiteDatabase db = getWritableDatabase();
+        if (db == null) {
+            return 0;
+        }
+        int match = URI_MATCHER.match(uri);
+        int deleted = 0;
+        switch (match) {
+        case SNAPSHOTS_ID: {
+            selection = DatabaseUtils.concatenateWhere(selection, TABLE_SNAPSHOTS + "._id=?");
+            selectionArgs = DatabaseUtils.appendSelectionArgs(selectionArgs,
+                    new String[] { Long.toString(ContentUris.parseId(uri)) });
+            // fall through
+        }
+        case SNAPSHOTS:
+            deleteDataFiles(db, selection, selectionArgs);
+            deleted = db.delete(TABLE_SNAPSHOTS, selection, selectionArgs);
+            break;
+        default:
+            throw new UnsupportedOperationException("Unknown delete URI " + uri);
+        }
+        if (deleted > 0) {
+            getContext().getContentResolver().notifyChange(uri, null, false);
+        }
+        return deleted;
+    }
+
+    @Override
+    public int update(Uri uri, ContentValues values, String selection,
+            String[] selectionArgs) {
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+}
diff --git a/src/com/android/browser/reflect/ReflectHelper.java b/src/com/android/browser/reflect/ReflectHelper.java
new file mode 100644
index 0000000..5a5f2ae
--- /dev/null
+++ b/src/com/android/browser/reflect/ReflectHelper.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2014, The Linux Foundation. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of The Linux Foundation nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+ * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+ * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+
+package com.android.browser.reflect;
+
+import android.util.Log;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Field;
+
+public class ReflectHelper {
+
+    private final static String LOGTAG = "ReflectHelper";
+
+    public static Object newObject(String className) {
+        Object obj = null;
+        try {
+            Class clazz = Class.forName(className);
+            obj = clazz.newInstance();
+        } catch (Exception e) {
+            Log.e(LOGTAG, "An exception occured : " + e.getMessage());
+        }
+        return obj;
+    }
+
+    public static Object newObject(String className, Class[] argTypes, Object[] args) {
+        if (args == null || args.length == 0) {
+            return newObject(className);
+        }
+        Object obj = null;
+        try {
+            Class clazz = Class.forName(className);
+            Constructor ctor = clazz.getDeclaredConstructor(argTypes);
+            obj = ctor.newInstance(args);
+        } catch (Exception e) {
+            Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+        }
+        return obj;
+    }
+
+    public static Object invokeMethod(Object obj, String method, Class[] argTypes, Object[] args) {
+        Object result  = null;
+        boolean modifiedAccessibility = false;
+        if (obj == null || method == null) {
+            throw new IllegalArgumentException("Object and Method must be supplied.");
+        }
+        try {
+            Method m = obj.getClass().getDeclaredMethod(method, argTypes);
+            if(m != null) {
+                // make it visible
+                if (!m.isAccessible()) {
+                    modifiedAccessibility = true;
+                    m.setAccessible(true);
+                }
+                result = m.invoke(obj, args);
+                if (modifiedAccessibility)
+                    m.setAccessible(false);
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+        }
+        return result;
+    }
+
+    public static Object invokeStaticMethod(String className, String method,
+                                            Class[] argTypes, Object[] args) {
+        Object result  = null;
+        boolean modifiedAccessibility = false;
+        if (className == null || method == null) {
+            throw new IllegalArgumentException("Object and Method must be supplied.");
+        }
+        try {
+            Class clazz = Class.forName(className);
+            Method m = clazz.getDeclaredMethod(method, argTypes);
+            if(m != null) {
+                // make it visible
+                if (!m.isAccessible()) {
+                    modifiedAccessibility = true;
+                    m.setAccessible(true);
+                }
+                result = m.invoke(null, args);
+                if (modifiedAccessibility)
+                    m.setAccessible(false);
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+        }
+        return result;
+    }
+
+    public static Object getStaticVariable(String className, String fieldName) {
+        Object result  = null;
+        boolean modifiedAccessibility = false;
+        try {
+            Class clazz = Class.forName(className);
+            Field f = clazz.getDeclaredField(fieldName);
+            if(f != null) {
+                if (!f.isAccessible()) {
+                    modifiedAccessibility = true;
+                    f.setAccessible(true);
+                }
+                f.setAccessible(true);
+                result = f.get(null);
+                if (modifiedAccessibility)
+                    f.setAccessible(false);
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+        }
+        return result;
+    }
+
+    public static Object getVariable(Object obj, String fieldName) {
+        Object result  = null;
+        boolean modifiedAccessibility = false;
+        try {
+            Class clazz = obj.getClass();
+            Field f = clazz.getDeclaredField(fieldName);
+            if(f != null) {
+                if (!f.isAccessible()) {
+                    modifiedAccessibility = true;
+                    f.setAccessible(true);
+                }
+                f.setAccessible(true);
+                result = f.get(obj);
+                if (modifiedAccessibility)
+                    f.setAccessible(false);
+            }
+        } catch (Exception e) {
+            Log.e(LOGTAG, "An exception occured : " + e.getMessage() );
+        }
+        return result;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/browser/search/DefaultSearchEngine.java b/src/com/android/browser/search/DefaultSearchEngine.java
new file mode 100644
index 0000000..7613377
--- /dev/null
+++ b/src/com/android/browser/search/DefaultSearchEngine.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2010 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.search;
+
+import android.app.PendingIntent;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.reflect.ReflectHelper;
+
+public class DefaultSearchEngine implements SearchEngine {
+
+    private static final String TAG = "DefaultSearchEngine";
+
+    private final SearchableInfo mSearchable;
+
+    private final CharSequence mLabel;
+
+    private DefaultSearchEngine(Context context, SearchableInfo searchable) {
+        mSearchable = searchable;
+        mLabel = loadLabel(context, mSearchable.getSearchActivity());
+    }
+
+    public static DefaultSearchEngine create(Context context) {
+        SearchManager searchManager =
+                (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+        ComponentName name = (ComponentName) ReflectHelper.invokeMethod(
+                              searchManager, "getWebSearchActivity", null, null);
+
+        if (name == null) return null;
+        SearchableInfo searchable = searchManager.getSearchableInfo(name);
+        if (searchable == null) return null;
+        return new DefaultSearchEngine(context, searchable);
+    }
+
+    private CharSequence loadLabel(Context context, ComponentName activityName) {
+        PackageManager pm = context.getPackageManager();
+        try {
+            ActivityInfo ai = pm.getActivityInfo(activityName, 0);
+            return ai.loadLabel(pm);
+        } catch (PackageManager.NameNotFoundException ex) {
+            Log.e(TAG, "Web search activity not found: " + activityName);
+            return null;
+        }
+    }
+
+    public String getName() {
+        String packageName = mSearchable.getSearchActivity().getPackageName();
+        // Use "google" as name to avoid showing Google twice (app + OpenSearch)
+        if ("com.google.android.googlequicksearchbox".equals(packageName)) {
+            return SearchEngine.GOOGLE;
+        } else if ("com.android.quicksearchbox".equals(packageName)) {
+            return SearchEngine.GOOGLE;
+        } else {
+            return packageName;
+        }
+    }
+
+    public CharSequence getLabel() {
+        return mLabel;
+    }
+
+    public void startSearch(Context context, String query,
+                            Bundle appData, String extraData) {
+        try {
+            Intent intent = new Intent(Intent.ACTION_WEB_SEARCH);
+            intent.setComponent(mSearchable.getSearchActivity());
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+            intent.putExtra(SearchManager.QUERY, query);
+            if (appData != null) {
+                intent.putExtra(SearchManager.APP_DATA, appData);
+            }
+            if (extraData != null) {
+                intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+            }
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+            Intent viewIntent = new Intent(Intent.ACTION_VIEW);
+            viewIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            viewIntent.setPackage(context.getPackageName());
+            PendingIntent pending = PendingIntent.getActivity(context, 0, viewIntent,
+                    PendingIntent.FLAG_ONE_SHOT);
+            intent.putExtra(SearchManager.EXTRA_WEB_SEARCH_PENDINGINTENT, pending);
+            context.startActivity(intent);
+        } catch (ActivityNotFoundException ex) {
+            Log.e(TAG, "Web search activity not found: " +
+                        mSearchable.getSearchActivity());
+        }
+    }
+
+    public Cursor getSuggestions(Context context, String query) {
+        SearchManager searchManager =
+                (SearchManager) context.getSystemService(Context.SEARCH_SERVICE);
+        Object[] params  = {mSearchable, query};
+        Class[] type = new Class[] {SearchableInfo.class, String.class};
+        Cursor cursor = (Cursor) ReflectHelper.invokeMethod(
+                                   searchManager, "getSuggestions", type, params);
+        return cursor;
+    }
+
+    public boolean supportsSuggestions() {
+        return !TextUtils.isEmpty(mSearchable.getSuggestAuthority());
+    }
+
+    public void close() {
+    }
+
+    @Override
+    public String toString() {
+        return "ActivitySearchEngine{" + mSearchable + "}";
+    }
+
+    @Override
+    public boolean wantsEmptyQuery() {
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/search/OpenSearchSearchEngine.java b/src/com/android/browser/search/OpenSearchSearchEngine.java
new file mode 100644
index 0000000..e600aa9
--- /dev/null
+++ b/src/com/android/browser/search/OpenSearchSearchEngine.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2010 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.search;
+
+import com.android.browser.R;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONException;
+
+import android.app.SearchManager;
+import android.content.Context;
+import android.content.Intent;
+import android.database.AbstractCursor;
+import android.database.Cursor;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.http.AndroidHttpClient;
+import android.os.Bundle;
+import android.provider.Browser;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+
+/**
+ * Provides search suggestions, if any, for a given web search provider.
+ */
+public class OpenSearchSearchEngine implements SearchEngine {
+
+    private static final String TAG = "OpenSearchSearchEngine";
+
+    private static final String USER_AGENT = "Android/1.0";
+    private static final int HTTP_TIMEOUT_MS = 1000;
+
+    // TODO: this should be defined somewhere
+    private static final String HTTP_TIMEOUT = "http.connection-manager.timeout";
+
+    // Indices of the columns in the below arrays.
+    private static final int COLUMN_INDEX_ID = 0;
+    private static final int COLUMN_INDEX_QUERY = 1;
+    private static final int COLUMN_INDEX_ICON = 2;
+    private static final int COLUMN_INDEX_TEXT_1 = 3;
+    private static final int COLUMN_INDEX_TEXT_2 = 4;
+
+    // The suggestion columns used. If you are adding a new entry to these arrays make sure to
+    // update the list of indices declared above.
+    private static final String[] COLUMNS = new String[] {
+        "_id",
+        SearchManager.SUGGEST_COLUMN_QUERY,
+        SearchManager.SUGGEST_COLUMN_ICON_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_2,
+    };
+
+    private static final String[] COLUMNS_WITHOUT_DESCRIPTION = new String[] {
+        "_id",
+        SearchManager.SUGGEST_COLUMN_QUERY,
+        SearchManager.SUGGEST_COLUMN_ICON_1,
+        SearchManager.SUGGEST_COLUMN_TEXT_1,
+    };
+
+    private final SearchEngineInfo mSearchEngineInfo;
+
+    private final AndroidHttpClient mHttpClient;
+
+    public OpenSearchSearchEngine(Context context, SearchEngineInfo searchEngineInfo) {
+        mSearchEngineInfo = searchEngineInfo;
+        mHttpClient = AndroidHttpClient.newInstance(USER_AGENT);
+        HttpParams params = mHttpClient.getParams();
+        params.setLongParameter(HTTP_TIMEOUT, HTTP_TIMEOUT_MS);
+    }
+
+    public String getName() {
+        return mSearchEngineInfo.getName();
+    }
+
+    public CharSequence getLabel() {
+        return mSearchEngineInfo.getLabel();
+    }
+
+    public void startSearch(Context context, String query, Bundle appData, String extraData) {
+        String uri = mSearchEngineInfo.getSearchUriForQuery(query);
+        if (uri == null) {
+            Log.e(TAG, "Unable to get search URI for " + mSearchEngineInfo);
+        } else {
+            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
+            // Make sure the intent goes to the Browser itself
+            intent.setPackage(context.getPackageName());
+            intent.addCategory(Intent.CATEGORY_DEFAULT);
+            intent.putExtra(SearchManager.QUERY, query);
+            if (appData != null) {
+                intent.putExtra(SearchManager.APP_DATA, appData);
+            }
+            if (extraData != null) {
+                intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+            }
+            intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
+            context.startActivity(intent);
+        }
+    }
+
+    /**
+     * Queries for a given search term and returns a cursor containing
+     * suggestions ordered by best match.
+     */
+    public Cursor getSuggestions(Context context, String query) {
+        if (TextUtils.isEmpty(query)) {
+            return null;
+        }
+        if (!isNetworkConnected(context)) {
+            Log.i(TAG, "Not connected to network.");
+            return null;
+        }
+
+        String suggestUri = mSearchEngineInfo.getSuggestUriForQuery(query);
+        if (TextUtils.isEmpty(suggestUri)) {
+            // No suggest URI available for this engine
+            return null;
+        }
+
+        try {
+            String content = readUrl(suggestUri);
+            if (content == null) return null;
+            /* The data format is a JSON array with items being regular strings or JSON arrays
+             * themselves. We are interested in the second and third elements, both of which
+             * should be JSON arrays. The second element/array contains the suggestions and the
+             * third element contains the descriptions. Some search engines don't support
+             * suggestion descriptions so the third element is optional.
+             */
+            JSONArray results = new JSONArray(content);
+            JSONArray suggestions = results.getJSONArray(1);
+            JSONArray descriptions = null;
+            if (results.length() > 2) {
+                descriptions = results.getJSONArray(2);
+                // Some search engines given an empty array "[]" for descriptions instead of
+                // not including it in the response.
+                if (descriptions.length() == 0) {
+                    descriptions = null;
+                }
+            }
+            return new SuggestionsCursor(suggestions, descriptions);
+        } catch (JSONException e) {
+            Log.w(TAG, "Error", e);
+        }
+        return null;
+    }
+
+    /**
+     * Executes a GET request and returns the response content.
+     *
+     * @param url Request URI.
+     * @return The response content. This is the empty string if the response
+     *         contained no content.
+     */
+    public String readUrl(String url) {
+        try {
+            HttpGet method = new HttpGet(url);
+            HttpResponse response = mHttpClient.execute(method);
+            if (response.getStatusLine().getStatusCode() == 200) {
+                return EntityUtils.toString(response.getEntity());
+            } else {
+                Log.i(TAG, "Suggestion request failed");
+                return null;
+            }
+        } catch (IOException e) {
+            Log.w(TAG, "Error", e);
+            return null;
+        }
+    }
+
+    public boolean supportsSuggestions() {
+        return mSearchEngineInfo.supportsSuggestions();
+    }
+
+    public void close() {
+        mHttpClient.close();
+    }
+
+    private boolean isNetworkConnected(Context context) {
+        NetworkInfo networkInfo = getActiveNetworkInfo(context);
+        return networkInfo != null && networkInfo.isConnected();
+    }
+
+    private NetworkInfo getActiveNetworkInfo(Context context) {
+        ConnectivityManager connectivity =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        if (connectivity == null) {
+            return null;
+        }
+        return connectivity.getActiveNetworkInfo();
+    }
+
+    private static class SuggestionsCursor extends AbstractCursor {
+
+        private final JSONArray mSuggestions;
+
+        private final JSONArray mDescriptions;
+
+        public SuggestionsCursor(JSONArray suggestions, JSONArray descriptions) {
+            mSuggestions = suggestions;
+            mDescriptions = descriptions;
+        }
+
+        @Override
+        public int getCount() {
+            return mSuggestions.length();
+        }
+
+        @Override
+        public String[] getColumnNames() {
+            return (mDescriptions != null ? COLUMNS : COLUMNS_WITHOUT_DESCRIPTION);
+        }
+
+        @Override
+        public String getString(int column) {
+            if (mPos != -1) {
+                if ((column == COLUMN_INDEX_QUERY) || (column == COLUMN_INDEX_TEXT_1)) {
+                    try {
+                        return mSuggestions.getString(mPos);
+                    } catch (JSONException e) {
+                        Log.w(TAG, "Error", e);
+                    }
+                } else if (column == COLUMN_INDEX_TEXT_2) {
+                    try {
+                        return mDescriptions.getString(mPos);
+                    } catch (JSONException e) {
+                        Log.w(TAG, "Error", e);
+                    }
+                } else if (column == COLUMN_INDEX_ICON) {
+                    return String.valueOf(R.drawable.magnifying_glass);
+                }
+            }
+            return null;
+        }
+
+        @Override
+        public double getDouble(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public float getFloat(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public int getInt(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public long getLong(int column) {
+            if (column == COLUMN_INDEX_ID) {
+                return mPos;        // use row# as the _Id
+            }
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public short getShort(int column) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public boolean isNull(int column) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "OpenSearchSearchEngine{" + mSearchEngineInfo + "}";
+    }
+
+    @Override
+    public boolean wantsEmptyQuery() {
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/search/SearchEngine.java b/src/com/android/browser/search/SearchEngine.java
new file mode 100644
index 0000000..8f2d58d
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngine.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2010 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.search;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+
+/**
+ * Interface for search engines.
+ */
+public interface SearchEngine {
+
+    // Used if the search engine is Google
+    static final String GOOGLE = "google";
+
+    /**
+     * Gets the unique name of this search engine.
+     */
+    public String getName();
+
+    /**
+     * Gets the human-readable name of this search engine.
+     */
+    public CharSequence getLabel();
+
+    /**
+     * Starts a search.
+     */
+    public void startSearch(Context context, String query, Bundle appData, String extraData);
+
+    /**
+     * Gets search suggestions.
+     */
+    public Cursor getSuggestions(Context context, String query);
+
+    /**
+     * Checks whether this search engine supports search suggestions.
+     */
+    public boolean supportsSuggestions();
+
+    /**
+     * Closes this search engine.
+     */
+    public void close();
+
+    /**
+     * Checks whether this search engine should be sent zero char query.
+     */
+    public boolean wantsEmptyQuery();
+}
diff --git a/src/com/android/browser/search/SearchEngineInfo.java b/src/com/android/browser/search/SearchEngineInfo.java
new file mode 100644
index 0000000..ec304f6
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngineInfo.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2010 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.search;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.NotFoundException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.browser.R;
+
+import java.net.URLEncoder;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Loads and holds data for a given web search engine.
+ */
+public class SearchEngineInfo {
+
+    private static String TAG = "SearchEngineInfo";
+
+    // The fields of a search engine data array, defined in the same order as they appear in the
+    // all_search_engines.xml file.
+    // If you are adding/removing to this list, remember to update NUM_FIELDS below.
+    private static final int FIELD_LABEL = 0;
+    private static final int FIELD_KEYWORD = 1;
+    private static final int FIELD_FAVICON_URI = 2;
+    private static final int FIELD_SEARCH_URI = 3;
+    private static final int FIELD_ENCODING = 4;
+    private static final int FIELD_SUGGEST_URI = 5;
+    private static final int NUM_FIELDS = 6;
+
+    // The OpenSearch URI template parameters that we support.
+    private static final String PARAMETER_LANGUAGE = "{language}";
+    private static final String PARAMETER_SEARCH_TERMS = "{searchTerms}";
+    private static final String PARAMETER_INPUT_ENCODING = "{inputEncoding}";
+
+    private final String mName;
+
+    // The array of strings defining this search engine. The array values are in the same order as
+    // the above enumeration definition.
+    private final String[] mSearchEngineData;
+
+    /**
+     * @throws IllegalArgumentException If the name does not refer to a valid search engine
+     */
+    public SearchEngineInfo(Context context, String name) throws IllegalArgumentException {
+        mName = name;
+        Resources res = context.getResources();
+
+        String packageName = R.class.getPackage().getName();
+        int id_data = res.getIdentifier(name, "array", packageName);
+        if (id_data == 0) {
+            throw new IllegalArgumentException("No resources found for " + name);
+        }
+        mSearchEngineData = res.getStringArray(id_data);
+
+        if (mSearchEngineData == null) {
+            throw new IllegalArgumentException("No data found for " + name);
+        }
+        if (mSearchEngineData.length != NUM_FIELDS) {
+                throw new IllegalArgumentException(
+                        name + " has invalid number of fields - " + mSearchEngineData.length);
+        }
+        if (TextUtils.isEmpty(mSearchEngineData[FIELD_SEARCH_URI])) {
+            throw new IllegalArgumentException(name + " has an empty search URI");
+        }
+
+        // Add the current language/country information to the URIs.
+        Locale locale = context.getResources().getConfiguration().locale;
+        StringBuilder language = new StringBuilder(locale.getLanguage());
+        if (!TextUtils.isEmpty(locale.getCountry())) {
+            language.append('-');
+            language.append(locale.getCountry());
+        }
+
+        String language_str = language.toString();
+        mSearchEngineData[FIELD_SEARCH_URI] =
+                mSearchEngineData[FIELD_SEARCH_URI].replace(PARAMETER_LANGUAGE, language_str);
+        mSearchEngineData[FIELD_SUGGEST_URI] =
+                mSearchEngineData[FIELD_SUGGEST_URI].replace(PARAMETER_LANGUAGE, language_str);
+
+        // Default to UTF-8 if not specified.
+        String enc = mSearchEngineData[FIELD_ENCODING];
+        if (TextUtils.isEmpty(enc)) {
+            enc = "UTF-8";
+            mSearchEngineData[FIELD_ENCODING] = enc;
+        }
+
+        // Add the input encoding method to the URI.
+        mSearchEngineData[FIELD_SEARCH_URI] =
+                mSearchEngineData[FIELD_SEARCH_URI].replace(PARAMETER_INPUT_ENCODING, enc);
+        mSearchEngineData[FIELD_SUGGEST_URI] =
+                mSearchEngineData[FIELD_SUGGEST_URI].replace(PARAMETER_INPUT_ENCODING, enc);
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getLabel() {
+        return mSearchEngineData[FIELD_LABEL];
+    }
+
+    /**
+     * Returns the URI for launching a web search with the given query (or null if there was no
+     * data available for this search engine).
+     */
+    public String getSearchUriForQuery(String query) {
+        return getFormattedUri(searchUri(), query);
+    }
+
+    /**
+     * Returns the URI for retrieving web search suggestions for the given query (or null if there
+     * was no data available for this search engine).
+     */
+    public String getSuggestUriForQuery(String query) {
+        return getFormattedUri(suggestUri(), query);
+    }
+
+    public boolean supportsSuggestions() {
+        return !TextUtils.isEmpty(suggestUri());
+    }
+
+    public String faviconUri() {
+        return mSearchEngineData[FIELD_FAVICON_URI];
+    }
+
+    private String suggestUri() {
+        return mSearchEngineData[FIELD_SUGGEST_URI];
+    }
+
+    private String searchUri() {
+        return mSearchEngineData[FIELD_SEARCH_URI];
+    }
+
+    /**
+     * Formats a launchable uri out of the template uri by replacing the template parameters with
+     * actual values.
+     */
+    private String getFormattedUri(String templateUri, String query) {
+        if (TextUtils.isEmpty(templateUri)) {
+            return null;
+        }
+
+        // Encode the query terms in the requested encoding (and fallback to UTF-8 if not).
+        String enc = mSearchEngineData[FIELD_ENCODING];
+        try {
+            return templateUri.replace(PARAMETER_SEARCH_TERMS, URLEncoder.encode(query, enc));
+        } catch (java.io.UnsupportedEncodingException e) {
+            Log.e(TAG, "Exception occured when encoding query " + query + " to " + enc);
+            return null;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "SearchEngineInfo{" + Arrays.toString(mSearchEngineData) + "}";
+    }
+
+}
diff --git a/src/com/android/browser/search/SearchEnginePreference.java b/src/com/android/browser/search/SearchEnginePreference.java
new file mode 100644
index 0000000..62ce97b
--- /dev/null
+++ b/src/com/android/browser/search/SearchEnginePreference.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2010 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.search;
+
+import com.android.browser.R;
+
+import android.app.SearchManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+class SearchEnginePreference extends ListPreference {
+
+    private static final String TAG = "SearchEnginePreference";
+
+    public SearchEnginePreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        ArrayList<CharSequence> entryValues = new ArrayList<CharSequence>();
+        ArrayList<CharSequence> entries = new ArrayList<CharSequence>();
+
+        SearchEngine defaultSearchEngine = SearchEngines.getDefaultSearchEngine(context);
+        String defaultSearchEngineName = null;
+        if (defaultSearchEngine != null) {
+            defaultSearchEngineName = defaultSearchEngine.getName();
+            entryValues.add(defaultSearchEngineName);
+            entries.add(defaultSearchEngine.getLabel());
+        }
+        for (SearchEngineInfo searchEngineInfo : SearchEngines.getSearchEngineInfos(context)) {
+            String name = searchEngineInfo.getName();
+            // Skip entry with same name as default provider
+            if (!name.equals(defaultSearchEngineName)) {
+                entryValues.add(name);
+                entries.add(searchEngineInfo.getLabel());
+            }
+        }
+
+        setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()]));
+        setEntries(entries.toArray(new CharSequence[entries.size()]));
+
+        //for other language the default search engine is google,but for English and
+        //Chinese the default search engine should be Baidu.
+        String language = context.getResources().getConfiguration().locale.toString();
+        if (language.equals("zh_CN")) {
+            setDefaultValue("baidu_cn");
+        } else if (language.equals("en_US")) {
+            setDefaultValue("baidu");
+        }
+    }
+}
diff --git a/src/com/android/browser/search/SearchEngines.java b/src/com/android/browser/search/SearchEngines.java
new file mode 100644
index 0000000..dff5f62
--- /dev/null
+++ b/src/com/android/browser/search/SearchEngines.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2010 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.search;
+
+import com.android.browser.R;
+import com.android.browser.reflect.ReflectHelper;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SearchEngines {
+
+    private static final String TAG = "SearchEngines";
+
+    public static SearchEngine getDefaultSearchEngine(Context context) {
+        return DefaultSearchEngine.create(context);
+    }
+
+    public static List<SearchEngineInfo> getSearchEngineInfos(Context context) {
+        ArrayList<SearchEngineInfo> searchEngineInfos = new ArrayList<SearchEngineInfo>();
+        Resources res = context.getResources();
+        String[] searchEngines = res.getStringArray(R.array.search_engines);
+        Object[] params  = { new String("persist.env.c.browser.resource"),
+                                 new String("default")};
+        Class[] type = new Class[] {String.class, String.class};
+        String browserRes = (String)ReflectHelper.invokeStaticMethod(
+                                "android.os.SystemProperties","get", type, params);
+        for (int i = 0; i < searchEngines.length; i++) {
+            String name = searchEngines[i];
+            if ("cmcc".equals(browserRes)) {
+                SearchEngineInfo info = new SearchEngineInfo(context, name);
+                searchEngineInfos.add(info);
+            } else if (!name.startsWith("cmcc")) {
+                SearchEngineInfo info = new SearchEngineInfo(context, name);
+                searchEngineInfos.add(info);
+            }
+        }
+        return searchEngineInfos;
+    }
+
+    public static SearchEngine get(Context context, String name) {
+        // TODO: cache
+        SearchEngine defaultSearchEngine = getDefaultSearchEngine(context);
+        if (TextUtils.isEmpty(name)
+                || (defaultSearchEngine != null && name.equals(defaultSearchEngine.getName()))) {
+            return defaultSearchEngine;
+        }
+        SearchEngineInfo searchEngineInfo = getSearchEngineInfo(context, name);
+        if (searchEngineInfo == null) return defaultSearchEngine;
+        return new OpenSearchSearchEngine(context, searchEngineInfo);
+    }
+
+    public static SearchEngineInfo getSearchEngineInfo(Context context, String name) {
+        try {
+            return new SearchEngineInfo(context, name);
+        } catch (IllegalArgumentException exception) {
+            Log.e(TAG, "Cannot load search engine " + name, exception);
+            return null;
+        }
+    }
+
+}
diff --git a/src/com/android/browser/stub/NullController.java b/src/com/android/browser/stub/NullController.java
new file mode 100644
index 0000000..149fe4e
--- /dev/null
+++ b/src/com/android/browser/stub/NullController.java
@@ -0,0 +1,152 @@
+package com.android.browser.stub;
+
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ContextMenu.ContextMenuInfo;
+
+import com.android.browser.ActivityController;
+
+
+public class NullController implements ActivityController {
+
+    public static NullController INSTANCE = new NullController();
+
+    private NullController() {}
+
+    @Override
+    public void start(Intent intent) {
+    }
+
+    @Override
+    public void onSaveInstanceState(Bundle outState) {
+    }
+
+    @Override
+    public void handleNewIntent(Intent intent) {
+    }
+
+    @Override
+    public void onResume() {
+    }
+
+    @Override
+    public boolean onMenuOpened(int featureId, Menu menu) {
+        return false;
+    }
+
+    @Override
+    public void onOptionsMenuClosed(Menu menu) {
+    }
+
+    @Override
+    public void onContextMenuClosed(Menu menu) {
+    }
+
+    @Override
+    public void onPause() {
+    }
+
+    @Override
+    public void onDestroy() {
+    }
+
+    @Override
+    public void onConfgurationChanged(Configuration newConfig) {
+    }
+
+    @Override
+    public void onLowMemory() {
+    }
+
+    @Override
+    public boolean onCreateOptionsMenu(Menu menu) {
+        return false;
+    }
+
+    @Override
+    public boolean onPrepareOptionsMenu(Menu menu) {
+        return false;
+    }
+
+    @Override
+    public boolean onOptionsItemSelected(MenuItem item) {
+        return false;
+    }
+
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View v,
+            ContextMenuInfo menuInfo) {
+
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public void onActionModeStarted(ActionMode mode) {
+    }
+
+    @Override
+    public void onActionModeFinished(ActionMode mode) {
+    }
+
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+    }
+
+    @Override
+    public boolean onSearchRequested() {
+        return false;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean dispatchKeyShortcutEvent(KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    public boolean dispatchTrackballEvent(MotionEvent ev) {
+        return false;
+    }
+
+    @Override
+    public boolean dispatchGenericMotionEvent(MotionEvent ev) {
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/util/ThreadedCursorAdapter.java b/src/com/android/browser/util/ThreadedCursorAdapter.java
new file mode 100644
index 0000000..f07a375
--- /dev/null
+++ b/src/com/android/browser/util/ThreadedCursorAdapter.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2011 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.util;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.os.Process;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Adapter;
+import android.widget.BaseAdapter;
+import android.widget.CursorAdapter;
+
+import com.android.browser.R;
+
+import java.lang.ref.WeakReference;
+
+public abstract class ThreadedCursorAdapter<T> extends BaseAdapter {
+
+    private static final String LOGTAG = "BookmarksThreadedAdapter";
+    private static final boolean DEBUG = false;
+
+    private Context mContext;
+    private Object mCursorLock = new Object();
+    private CursorAdapter mCursorAdapter;
+    private T mLoadingObject;
+    private Handler mLoadHandler;
+    private Handler mHandler;
+    private int mSize;
+    private boolean mHasCursor;
+    private long mGeneration;
+
+    private class LoadContainer {
+        WeakReference<View> view;
+        int position;
+        T bind_object;
+        Adapter owner;
+        boolean loaded;
+        long generation;
+    }
+
+    public ThreadedCursorAdapter(Context context, Cursor c) {
+        mContext = context;
+        mHasCursor = (c != null);
+        mCursorAdapter = new CursorAdapter(context, c, 0) {
+
+            @Override
+            public View newView(Context context, Cursor cursor, ViewGroup parent) {
+                throw new IllegalStateException("not supported");
+            }
+
+            @Override
+            public void bindView(View view, Context context, Cursor cursor) {
+                throw new IllegalStateException("not supported");
+            }
+
+            @Override
+            public void notifyDataSetChanged() {
+                super.notifyDataSetChanged();
+                mSize = getCount();
+                mGeneration++;
+                ThreadedCursorAdapter.this.notifyDataSetChanged();
+            }
+
+            @Override
+            public void notifyDataSetInvalidated() {
+                super.notifyDataSetInvalidated();
+                mSize = getCount();
+                mGeneration++;
+                ThreadedCursorAdapter.this.notifyDataSetInvalidated();
+            }
+
+        };
+        mSize = mCursorAdapter.getCount();
+        HandlerThread thread = new HandlerThread("threaded_adapter_" + this,
+                Process.THREAD_PRIORITY_BACKGROUND);
+        thread.start();
+        mLoadHandler = new Handler(thread.getLooper()) {
+            @SuppressWarnings("unchecked")
+            @Override
+            public void handleMessage(Message msg) {
+                if (DEBUG) {
+                    Log.d(LOGTAG, "loading: " + msg.what);
+                }
+                loadRowObject(msg.what, (LoadContainer) msg.obj);
+            }
+        };
+        mHandler = new Handler() {
+            @Override
+            public void handleMessage(Message msg) {
+                @SuppressWarnings("unchecked")
+                LoadContainer container = (LoadContainer) msg.obj;
+                if (container == null) {
+                    return;
+                }
+                View view = container.view.get();
+                if (view == null
+                        || container.owner != ThreadedCursorAdapter.this
+                        || container.position != msg.what
+                        || view.getWindowToken() == null
+                        || container.generation != mGeneration) {
+                    return;
+                }
+                container.loaded = true;
+                bindView(view, container.bind_object);
+            }
+        };
+    }
+
+    @Override
+    public int getCount() {
+        return mSize;
+    }
+
+    @Override
+    public Cursor getItem(int position) {
+        return (Cursor) mCursorAdapter.getItem(position);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        synchronized (mCursorLock) {
+            return getItemId(getItem(position));
+        }
+    }
+
+    private void loadRowObject(int position, LoadContainer container) {
+        if (container == null
+                || container.position != position
+                || container.owner != ThreadedCursorAdapter.this
+                || container.view.get() == null) {
+            return;
+        }
+        synchronized (mCursorLock) {
+            Cursor c = (Cursor) mCursorAdapter.getItem(position);
+            if (c == null || c.isClosed()) {
+                return;
+            }
+            container.bind_object = getRowObject(c, container.bind_object);
+        }
+        mHandler.obtainMessage(position, container).sendToTarget();
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (convertView == null) {
+            convertView = newView(mContext, parent);
+        }
+        @SuppressWarnings("unchecked")
+        LoadContainer container = (LoadContainer) convertView.getTag(R.id.load_object);
+        if (container == null) {
+            container = new LoadContainer();
+            container.view = new WeakReference<View>(convertView);
+            convertView.setTag(R.id.load_object, container);
+        }
+        if (container.position == position
+                && container.owner == this
+                && container.loaded
+                && container.generation == mGeneration) {
+            bindView(convertView, container.bind_object);
+        } else {
+            bindView(convertView, cachedLoadObject());
+            if (mHasCursor) {
+                container.position = position;
+                container.loaded = false;
+                container.owner = this;
+                container.generation = mGeneration;
+                mLoadHandler.obtainMessage(position, container).sendToTarget();
+            }
+        }
+        return convertView;
+    }
+
+    private T cachedLoadObject() {
+        if (mLoadingObject == null) {
+            mLoadingObject = getLoadingObject();
+        }
+        return mLoadingObject;
+    }
+
+    public void changeCursor(Cursor cursor) {
+        mLoadHandler.removeCallbacksAndMessages(null);
+        mHandler.removeCallbacksAndMessages(null);
+        synchronized (mCursorLock) {
+            mHasCursor = (cursor != null);
+            mCursorAdapter.changeCursor(cursor);
+        }
+    }
+
+    public abstract View newView(Context context, ViewGroup parent);
+    public abstract void bindView(View view, T object);
+    public abstract T getRowObject(Cursor c, T recycleObject);
+    public abstract T getLoadingObject();
+    protected abstract long getItemId(Cursor c);
+}
\ No newline at end of file
diff --git a/src/com/android/browser/view/BasePieView.java b/src/com/android/browser/view/BasePieView.java
new file mode 100644
index 0000000..b9178be
--- /dev/null
+++ b/src/com/android/browser/view/BasePieView.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.Adapter;
+
+import java.util.ArrayList;
+
+/**
+ * common code for pie views
+ */
+public abstract class BasePieView implements PieMenu.PieView {
+
+    protected Adapter mAdapter;
+    private DataSetObserver mObserver;
+    protected ArrayList<View> mViews;
+
+    protected OnLayoutListener mListener;
+
+    protected int mCurrent;
+    protected int mChildWidth;
+    protected int mChildHeight;
+    protected int mWidth;
+    protected int mHeight;
+    protected int mLeft;
+    protected int mTop;
+
+    public BasePieView() {
+    }
+
+    public void setLayoutListener(OnLayoutListener l) {
+        mListener = l;
+    }
+
+    public void setAdapter(Adapter adapter) {
+        mAdapter = adapter;
+        if (adapter == null) {
+            if (mAdapter != null) {
+                mAdapter.unregisterDataSetObserver(mObserver);
+            }
+            mViews = null;
+            mCurrent = -1;
+        } else {
+            mObserver = new DataSetObserver() {
+                @Override
+                public void onChanged() {
+                    buildViews();
+                }
+
+                @Override
+                public void onInvalidated() {
+                    mViews.clear();
+                }
+            };
+            mAdapter.registerDataSetObserver(mObserver);
+            setCurrent(0);
+        }
+    }
+
+    public void setCurrent(int ix) {
+        mCurrent = ix;
+    }
+
+    public Adapter getAdapter() {
+        return mAdapter;
+    }
+
+    protected void buildViews() {
+        if (mAdapter != null) {
+            final int n = mAdapter.getCount();
+            if (mViews == null) {
+                mViews = new ArrayList<View>(n);
+            } else {
+                mViews.clear();
+            }
+            mChildWidth = 0;
+            mChildHeight = 0;
+            for (int i = 0; i < n; i++) {
+                View view = mAdapter.getView(i, null, null);
+                view.measure(View.MeasureSpec.UNSPECIFIED,
+                        View.MeasureSpec.UNSPECIFIED);
+                mChildWidth = Math.max(mChildWidth, view.getMeasuredWidth());
+                mChildHeight = Math.max(mChildHeight, view.getMeasuredHeight());
+                mViews.add(view);
+            }
+        }
+    }
+
+    /**
+     * this will be called before the first draw call
+     * needs to set top, left, width, height
+     */
+    @Override
+    public void layout(int anchorX, int anchorY, boolean left, float angle,
+            int parentHeight) {
+        if (mListener != null) {
+            mListener.onLayout(anchorX, anchorY, left);
+        }
+    }
+
+
+    @Override
+    public abstract void draw(Canvas canvas);
+
+    protected void drawView(View view, Canvas canvas) {
+        final int state = canvas.save();
+        canvas.translate(view.getLeft(), view.getTop());
+        view.draw(canvas);
+        canvas.restoreToCount(state);
+    }
+
+    protected abstract int findChildAt(int y);
+
+    @Override
+    public boolean onTouchEvent(MotionEvent evt) {
+        int action = evt.getActionMasked();
+        int evtx = (int) evt.getX();
+        int evty = (int) evt.getY();
+        if ((evtx < mLeft) || (evtx >= mLeft + mWidth)
+                || (evty < mTop) || (evty >= mTop + mHeight)) {
+            return false;
+        }
+        switch (action) {
+            case MotionEvent.ACTION_MOVE:
+                View v = mViews.get(mCurrent);
+                setCurrent(Math.max(0, Math.min(mViews.size() -1,
+                        findChildAt(evty))));
+                View v1 = mViews.get(mCurrent);
+                if (v != v1) {
+                    v.setPressed(false);
+                    v1.setPressed(true);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                mViews.get(mCurrent).performClick();
+                mViews.get(mCurrent).setPressed(false);
+                break;
+            default:
+                break;
+        }
+        return true;
+    }
+
+}
diff --git a/src/com/android/browser/view/BookmarkContainer.java b/src/com/android/browser/view/BookmarkContainer.java
new file mode 100644
index 0000000..5175589
--- /dev/null
+++ b/src/com/android/browser/view/BookmarkContainer.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.widget.RelativeLayout;
+
+public class BookmarkContainer extends RelativeLayout implements OnClickListener {
+
+    private OnClickListener mClickListener;
+    private boolean mIgnoreRequestLayout = false;
+
+    public BookmarkContainer(Context context) {
+        super(context);
+        init();
+    }
+
+    public BookmarkContainer(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init();
+    }
+
+    public BookmarkContainer(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init();
+    }
+
+    void init() {
+        setFocusable(true);
+        super.setOnClickListener(this);
+    }
+
+    @Override
+    public void setBackgroundDrawable(Drawable d) {
+        super.setBackgroundDrawable(d);
+    }
+
+    @Override
+    public void setOnClickListener(OnClickListener l) {
+        mClickListener = l;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        updateTransitionDrawable(isPressed());
+    }
+
+    void updateTransitionDrawable(boolean pressed) {
+        final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+        Drawable selector = getBackground();
+        if (selector != null && selector instanceof StateListDrawable) {
+            Drawable d = ((StateListDrawable)selector).getCurrent();
+            if (d != null && d instanceof TransitionDrawable) {
+                if (pressed && isLongClickable()) {
+                    ((TransitionDrawable) d).startTransition(longPressTimeout);
+                } else {
+                    ((TransitionDrawable) d).resetTransition();
+                }
+            }
+        }
+    }
+
+    @Override
+    public void onClick(View view) {
+        updateTransitionDrawable(false);
+        if (mClickListener != null) {
+            mClickListener.onClick(view);
+        }
+    }
+
+    public void setIgnoreRequestLayout(boolean ignore) {
+        mIgnoreRequestLayout = ignore;
+    }
+
+    @Override
+    public void requestLayout() {
+        if (!mIgnoreRequestLayout) {
+            super.requestLayout();
+        }
+    }
+}
diff --git a/src/com/android/browser/view/BookmarkExpandableView.java b/src/com/android/browser/view/BookmarkExpandableView.java
new file mode 100644
index 0000000..763efa7
--- /dev/null
+++ b/src/com/android/browser/view/BookmarkExpandableView.java
@@ -0,0 +1,473 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.util.AttributeSet;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseExpandableListAdapter;
+import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.browser.BreadCrumbView;
+import com.android.browser.BrowserBookmarksAdapter;
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+public class BookmarkExpandableView extends ExpandableListView
+        implements BreadCrumbView.Controller {
+
+    public static final String LOCAL_ACCOUNT_NAME = "local";
+
+    private BookmarkAccountAdapter mAdapter;
+    private int mColumnWidth;
+    private Context mContext;
+    private OnChildClickListener mOnChildClickListener;
+    private ContextMenuInfo mContextMenuInfo = null;
+    private OnCreateContextMenuListener mOnCreateContextMenuListener;
+    private boolean mLongClickable;
+    private BreadCrumbView.Controller mBreadcrumbController;
+    private int mMaxColumnCount;
+
+    public BookmarkExpandableView(Context context) {
+        super(context);
+        init(context);
+    }
+
+    public BookmarkExpandableView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    public BookmarkExpandableView(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    void init(Context context) {
+        mContext = context;
+        setItemsCanFocus(true);
+        setLongClickable(false);
+        mMaxColumnCount = mContext.getResources()
+                .getInteger(R.integer.max_bookmark_columns);
+        setScrollBarStyle(SCROLLBARS_OUTSIDE_OVERLAY);
+        mAdapter = new BookmarkAccountAdapter(mContext);
+        super.setAdapter(mAdapter);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        if (width > 0) {
+            mAdapter.measureChildren(width);
+            setPadding(mAdapter.mRowPadding, 0, mAdapter.mRowPadding, 0);
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, widthMode);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        if (width != getMeasuredWidth()) {
+            mAdapter.measureChildren(getMeasuredWidth());
+        }
+    }
+
+    @Override
+    public void setAdapter(ExpandableListAdapter adapter) {
+        throw new RuntimeException("Not supported");
+    }
+
+    public void setColumnWidthFromLayout(int layout) {
+        LayoutInflater infalter = LayoutInflater.from(mContext);
+        View v = infalter.inflate(layout, this, false);
+        v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        mColumnWidth = v.getMeasuredWidth();
+    }
+
+    public void clearAccounts() {
+        mAdapter.clear();
+    }
+
+    public void addAccount(String accountName, BrowserBookmarksAdapter adapter,
+            boolean expandGroup) {
+        // First, check if it already exists
+        int indexOf = mAdapter.mGroups.indexOf(accountName);
+        if (indexOf >= 0) {
+            BrowserBookmarksAdapter existing = mAdapter.mChildren.get(indexOf);
+            if (existing != adapter) {
+                existing.unregisterDataSetObserver(mAdapter.mObserver);
+                // Replace the existing one
+                mAdapter.mChildren.remove(indexOf);
+                mAdapter.mChildren.add(indexOf, adapter);
+                adapter.registerDataSetObserver(mAdapter.mObserver);
+            }
+        } else {
+            mAdapter.mGroups.add(accountName);
+            mAdapter.mChildren.add(adapter);
+            adapter.registerDataSetObserver(mAdapter.mObserver);
+        }
+        mAdapter.notifyDataSetChanged();
+        if (expandGroup) {
+            expandGroup(mAdapter.getGroupCount() - 1);
+        }
+    }
+
+    @Override
+    public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
+        mOnChildClickListener = onChildClickListener;
+    }
+
+    @Override
+    public void setOnCreateContextMenuListener(OnCreateContextMenuListener l) {
+        mOnCreateContextMenuListener = l;
+        if (!mLongClickable) {
+            mLongClickable = true;
+            if (mAdapter != null) {
+                mAdapter.notifyDataSetChanged();
+            }
+        }
+    }
+
+    // SWE: com.android.internal.view.menu.MenuBuilder is a hidden class in SDK.
+    // Since the 'menu' object is of type MenuBuilder, java reflection method
+    // is the only way to access MenuBuilder.setCurrentMenuInfo().
+    static void setCurrentMenuInfo(ContextMenu menu, ContextMenuInfo menuInfo) {
+        try {
+            Class<?> proxyClass = Class.forName("com.android.internal.view.menu.MenuBuilder");
+            Class<?> argTypes[] = new Class[1];
+            argTypes[0] = ContextMenuInfo.class;
+            Method m =  proxyClass.getDeclaredMethod("setCurrentMenuInfo", argTypes);
+            m.setAccessible(true);
+
+            Object args[] = new Object[1];
+            args[0] = menuInfo;
+            m.invoke(menu, args);
+        } catch (Exception e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void createContextMenu(ContextMenu menu) {
+        // The below is copied from View - we want to bypass the override
+        // in AbsListView
+
+        ContextMenuInfo menuInfo = getContextMenuInfo();
+
+        // Sets the current menu info so all items added to menu will have
+        // my extra info set.
+        setCurrentMenuInfo(menu, menuInfo);
+
+        onCreateContextMenu(menu);
+        if (mOnCreateContextMenuListener != null) {
+            mOnCreateContextMenuListener.onCreateContextMenu(menu, this, menuInfo);
+        }
+
+        // Clear the extra information so subsequent items that aren't mine don't
+        // have my extra info.
+        setCurrentMenuInfo(menu, null);
+
+        if (getParent() != null) {
+            getParent().createContextMenu(menu);
+        }
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView) {
+        int groupPosition = (Integer) originalView.getTag(R.id.group_position);
+        int childPosition = (Integer) originalView.getTag(R.id.child_position);
+
+        mContextMenuInfo = new BookmarkContextMenuInfo(childPosition,
+                groupPosition);
+        if (getParent() != null) {
+            getParent().showContextMenuForChild(this);
+        }
+
+        return true;
+    }
+
+    @Override
+    public void onTop(BreadCrumbView view, int level, Object data) {
+        if (mBreadcrumbController != null) {
+            mBreadcrumbController.onTop(view, level, data);
+        }
+    }
+
+    public void setBreadcrumbController(BreadCrumbView.Controller controller) {
+        mBreadcrumbController = controller;
+    }
+
+    @Override
+    protected ContextMenuInfo getContextMenuInfo() {
+        return mContextMenuInfo;
+    }
+
+    public BrowserBookmarksAdapter getChildAdapter(int groupPosition) {
+        return mAdapter.mChildren.get(groupPosition);
+    }
+
+    private OnClickListener mChildClickListener = new OnClickListener() {
+
+        @Override
+        public void onClick(View v) {
+            if (v.getVisibility() != View.VISIBLE) {
+                return;
+            }
+            int groupPosition = (Integer) v.getTag(R.id.group_position);
+            int childPosition = (Integer) v.getTag(R.id.child_position);
+            if (mAdapter.getGroupCount() <= groupPosition
+                    || mAdapter.mChildren.get(groupPosition).getCount() <= childPosition) {
+                return;
+            }
+            long id = mAdapter.mChildren.get(groupPosition).getItemId(childPosition);
+            if (mOnChildClickListener != null) {
+                mOnChildClickListener.onChildClick(BookmarkExpandableView.this,
+                        v, groupPosition, childPosition, id);
+            }
+        }
+    };
+
+    private OnClickListener mGroupOnClickListener = new OnClickListener() {
+
+        @Override
+        public void onClick(View v) {
+            int groupPosition = (Integer) v.getTag(R.id.group_position);
+            if (isGroupExpanded(groupPosition)) {
+                collapseGroup(groupPosition);
+            } else {
+                expandGroup(groupPosition, true);
+            }
+        }
+    };
+
+    public BreadCrumbView getBreadCrumbs(int groupPosition) {
+        return mAdapter.getBreadCrumbView(groupPosition);
+    }
+
+    public JSONObject saveGroupState() throws JSONException {
+        JSONObject obj = new JSONObject();
+        int count = mAdapter.getGroupCount();
+        for (int i = 0; i < count; i++) {
+            String acctName = mAdapter.mGroups.get(i);
+            if (!isGroupExpanded(i)) {
+                obj.put(acctName != null ? acctName : LOCAL_ACCOUNT_NAME, false);
+            }
+        }
+        return obj;
+    }
+
+    class BookmarkAccountAdapter extends BaseExpandableListAdapter {
+        ArrayList<BrowserBookmarksAdapter> mChildren;
+        ArrayList<String> mGroups;
+        HashMap<Integer, BreadCrumbView> mBreadcrumbs =
+                new HashMap<Integer, BreadCrumbView>();
+        LayoutInflater mInflater;
+        int mRowCount = 1; // assume at least 1 child fits in a row
+        int mLastViewWidth = -1;
+        int mRowPadding = -1;
+        DataSetObserver mObserver = new DataSetObserver() {
+            @Override
+            public void onChanged() {
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public void onInvalidated() {
+                notifyDataSetInvalidated();
+            }
+        };
+
+        public BookmarkAccountAdapter(Context context) {
+            mContext = context;
+            mInflater = LayoutInflater.from(mContext);
+            mChildren = new ArrayList<BrowserBookmarksAdapter>();
+            mGroups = new ArrayList<String>();
+        }
+
+        public void clear() {
+            mGroups.clear();
+            mChildren.clear();
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public Object getChild(int groupPosition, int childPosition) {
+            return mChildren.get(groupPosition).getItem(childPosition);
+        }
+
+        @Override
+        public long getChildId(int groupPosition, int childPosition) {
+            return childPosition;
+        }
+
+        @Override
+        public View getChildView(int groupPosition, int childPosition,
+                boolean isLastChild, View convertView, ViewGroup parent) {
+            if (convertView == null) {
+                convertView = mInflater.inflate(R.layout.bookmark_grid_row, parent, false);
+            }
+            BrowserBookmarksAdapter childAdapter = mChildren.get(groupPosition);
+            int rowCount = mRowCount;
+            LinearLayout row = (LinearLayout) convertView;
+            if (row.getChildCount() > rowCount) {
+                row.removeViews(rowCount, row.getChildCount() - rowCount);
+            }
+            for (int i = 0; i < rowCount; i++) {
+                View cv = null;
+                if (row.getChildCount() > i) {
+                    cv = row.getChildAt(i);
+                }
+                int realChildPosition = (childPosition * rowCount) + i;
+                if (realChildPosition < childAdapter.getCount()) {
+                    View v = childAdapter.getView(realChildPosition, cv, row);
+                    v.setTag(R.id.group_position, groupPosition);
+                    v.setTag(R.id.child_position, realChildPosition);
+                    v.setOnClickListener(mChildClickListener);
+                    v.setLongClickable(mLongClickable);
+                    if (cv == null) {
+                        row.addView(v);
+                    } else if (cv != v) {
+                        row.removeViewAt(i);
+                        row.addView(v, i);
+                    } else {
+                        cv.setVisibility(View.VISIBLE);
+                    }
+                } else if (cv != null) {
+                    cv.setVisibility(View.GONE);
+                }
+            }
+            return row;
+        }
+
+        @Override
+        public int getChildrenCount(int groupPosition) {
+            BrowserBookmarksAdapter adapter = mChildren.get(groupPosition);
+            return (int) Math.ceil(adapter.getCount() / (float)mRowCount);
+        }
+
+        @Override
+        public Object getGroup(int groupPosition) {
+            return mChildren.get(groupPosition);
+        }
+
+        @Override
+        public int getGroupCount() {
+            return mGroups.size();
+        }
+
+        public void measureChildren(int viewWidth) {
+            if (mLastViewWidth == viewWidth) return;
+
+            int rowCount = viewWidth / mColumnWidth;
+            if (mMaxColumnCount > 0) {
+                rowCount = Math.min(rowCount, mMaxColumnCount);
+            }
+            int rowPadding = (viewWidth - (rowCount * mColumnWidth)) / 2;
+            boolean notify = rowCount != mRowCount || rowPadding != mRowPadding;
+            mRowCount = rowCount;
+            mRowPadding = rowPadding;
+            mLastViewWidth = viewWidth;
+            if (notify) {
+                notifyDataSetChanged();
+            }
+        }
+
+        @Override
+        public long getGroupId(int groupPosition) {
+            return groupPosition;
+        }
+
+        @Override
+        public View getGroupView(int groupPosition, boolean isExpanded,
+                View view, ViewGroup parent) {
+            if (view == null) {
+                view = mInflater.inflate(R.layout.bookmark_group_view, parent, false);
+                view.setOnClickListener(mGroupOnClickListener);
+            }
+            view.setTag(R.id.group_position, groupPosition);
+            FrameLayout crumbHolder = (FrameLayout) view.findViewById(R.id.crumb_holder);
+            crumbHolder.removeAllViews();
+            BreadCrumbView crumbs = getBreadCrumbView(groupPosition);
+            if (crumbs.getParent() != null) {
+                ((ViewGroup)crumbs.getParent()).removeView(crumbs);
+            }
+            crumbHolder.addView(crumbs);
+            TextView name = (TextView) view.findViewById(R.id.group_name);
+            String groupName = mGroups.get(groupPosition);
+            if (groupName == null) {
+                groupName = mContext.getString(R.string.local_bookmarks);
+            }
+            name.setText(groupName);
+            return view;
+        }
+
+        public BreadCrumbView getBreadCrumbView(int groupPosition) {
+            BreadCrumbView crumbs = mBreadcrumbs.get(groupPosition);
+            if (crumbs == null) {
+                crumbs = (BreadCrumbView)
+                        mInflater.inflate(R.layout.bookmarks_header, null);
+                crumbs.setController(BookmarkExpandableView.this);
+                crumbs.setUseBackButton(true);
+                crumbs.setMaxVisible(1);
+                String bookmarks = mContext.getString(R.string.bookmarks);
+                crumbs.pushView(bookmarks, false,
+                        BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER);
+                crumbs.setTag(R.id.group_position, groupPosition);
+                crumbs.setVisibility(View.GONE);
+                mBreadcrumbs.put(groupPosition, crumbs);
+            }
+            return crumbs;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return false;
+        }
+
+        @Override
+        public boolean isChildSelectable(int groupPosition, int childPosition) {
+            return true;
+        }
+    }
+
+    public static class BookmarkContextMenuInfo implements ContextMenuInfo {
+
+        private BookmarkContextMenuInfo(int childPosition, int groupPosition) {
+            this.childPosition = childPosition;
+            this.groupPosition = groupPosition;
+        }
+
+        public int childPosition;
+        public int groupPosition;
+    }
+
+}
diff --git a/src/com/android/browser/view/CustomScreenLinearLayout.java b/src/com/android/browser/view/CustomScreenLinearLayout.java
new file mode 100644
index 0000000..f5341e8
--- /dev/null
+++ b/src/com/android/browser/view/CustomScreenLinearLayout.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.LinearLayout;
+
+
+public class CustomScreenLinearLayout extends LinearLayout {
+
+    public CustomScreenLinearLayout(Context context) {
+        super(context);
+        setChildrenDrawingOrderEnabled(true);
+    }
+
+    public CustomScreenLinearLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setChildrenDrawingOrderEnabled(true);
+    }
+
+    public CustomScreenLinearLayout(Context context, AttributeSet attrs,
+            int defStyle) {
+        super(context, attrs, defStyle);
+        setChildrenDrawingOrderEnabled(true);
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        return childCount - i - 1;
+    }
+
+}
diff --git a/src/com/android/browser/view/EventRedirectingFrameLayout.java b/src/com/android/browser/view/EventRedirectingFrameLayout.java
new file mode 100644
index 0000000..901b021
--- /dev/null
+++ b/src/com/android/browser/view/EventRedirectingFrameLayout.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+public class EventRedirectingFrameLayout extends FrameLayout {
+
+    private int mTargetChild;
+
+    public EventRedirectingFrameLayout(Context context) {
+        super(context);
+    }
+
+    public EventRedirectingFrameLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public EventRedirectingFrameLayout(
+            Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public void setTargetChild(int index) {
+        if (index >= 0 && index < getChildCount()) {
+            mTargetChild = index;
+            getChildAt(mTargetChild).requestFocus();
+        }
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent ev) {
+        View child = getChildAt(mTargetChild);
+        if (child != null)
+            return child.dispatchTouchEvent(ev);
+        return false;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        View child = getChildAt(mTargetChild);
+        if (child != null)
+            return child.dispatchKeyEvent(event);
+        return false;
+    }
+
+    @Override
+    public boolean dispatchKeyEventPreIme(KeyEvent event) {
+        View child = getChildAt(mTargetChild);
+        if (child != null)
+            return child.dispatchKeyEventPreIme(event);
+        return false;
+    }
+
+}
diff --git a/src/com/android/browser/view/PieItem.java b/src/com/android/browser/view/PieItem.java
new file mode 100644
index 0000000..9e04ecb
--- /dev/null
+++ b/src/com/android/browser/view/PieItem.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.view.View;
+
+import com.android.browser.view.PieMenu.PieView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Pie menu item
+ */
+public class PieItem {
+
+    private View mView;
+    private PieView mPieView;
+    private int level;
+    private float start;
+    private float sweep;
+    private float animate;
+    private int inner;
+    private int outer;
+    private boolean mSelected;
+    private boolean mEnabled;
+    private List<PieItem> mItems;
+
+    public PieItem(View view, int level) {
+        mView = view;
+        this.level = level;
+        mEnabled = true;
+        setAnimationAngle(getAnimationAngle());
+        setAlpha(getAlpha());
+    }
+
+    public PieItem(View view, int level, PieView sym) {
+        mView = view;
+        this.level = level;
+        mPieView = sym;
+        mEnabled = false;
+    }
+
+    public boolean hasItems() {
+        return mItems != null;
+    }
+
+    public List<PieItem> getItems() {
+        return mItems;
+    }
+
+    public void addItem(PieItem item) {
+        if (mItems == null) {
+            mItems = new ArrayList<PieItem>();
+        }
+        mItems.add(item);
+    }
+
+    public void setAlpha(float alpha) {
+        if (mView != null) {
+            mView.setAlpha(alpha);
+        }
+    }
+
+    public float getAlpha() {
+        if (mView != null) {
+            return mView.getAlpha();
+        }
+        return 1;
+    }
+
+    public void setAnimationAngle(float a) {
+        animate = a;
+    }
+
+    public float getAnimationAngle() {
+        return animate;
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled = enabled;
+    }
+
+    public void setSelected(boolean s) {
+        mSelected = s;
+        if (mView != null) {
+            mView.setSelected(s);
+        }
+    }
+
+    public boolean isSelected() {
+        return mSelected;
+    }
+
+    public int getLevel() {
+        return level;
+    }
+
+    public void setGeometry(float st, float sw, int inside, int outside) {
+        start = st;
+        sweep = sw;
+        inner = inside;
+        outer = outside;
+    }
+
+    public float getStart() {
+        return start;
+    }
+
+    public float getStartAngle() {
+        return start + animate;
+    }
+
+    public float getSweep() {
+        return sweep;
+    }
+
+    public int getInnerRadius() {
+        return inner;
+    }
+
+    public int getOuterRadius() {
+        return outer;
+    }
+
+    public boolean isPieView() {
+        return (mPieView != null);
+    }
+
+    public View getView() {
+        return mView;
+    }
+
+    public void setPieView(PieView sym) {
+        mPieView = sym;
+    }
+
+    public PieView getPieView() {
+        if (mEnabled) {
+            return mPieView;
+        }
+        return null;
+    }
+
+}
diff --git a/src/com/android/browser/view/PieListView.java b/src/com/android/browser/view/PieListView.java
new file mode 100644
index 0000000..1043fc7
--- /dev/null
+++ b/src/com/android/browser/view/PieListView.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.view.View;
+
+/**
+ * shows views in a menu style list
+ */
+public class PieListView extends BasePieView {
+
+    private Paint mBgPaint;
+
+    public PieListView(Context ctx) {
+        mBgPaint = new Paint();
+        mBgPaint.setColor(ctx.getResources().getColor(R.color.qcMenuBackground));
+    }
+
+    /**
+     * this will be called before the first draw call
+     */
+    @Override
+    public void layout(int anchorX, int anchorY, boolean left, float angle,
+            int pHeight) {
+        super.layout(anchorX, anchorY, left, angle, pHeight);
+        buildViews();
+        mWidth = mChildWidth;
+        mHeight = mChildHeight * mAdapter.getCount();
+        mLeft = anchorX + (left ? 0 : - mChildWidth);
+        mTop = Math.max(anchorY - mHeight / 2, 0);
+        if (mTop + mHeight > pHeight) {
+            mTop = pHeight - mHeight;
+        }
+        if (mViews != null) {
+            layoutChildrenLinear();
+        }
+    }
+
+    protected void layoutChildrenLinear() {
+        final int n = mViews.size();
+        int top = mTop;
+        for (View view : mViews) {
+            view.layout(mLeft, top, mLeft + mChildWidth, top + mChildHeight);
+            top += mChildHeight;
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        canvas.drawRect(mLeft, mTop, mLeft + mWidth, mTop + mHeight, mBgPaint);
+        if (mViews != null) {
+            for (View view : mViews) {
+                drawView(view, canvas);
+            }
+        }
+    }
+
+    @Override
+    protected int findChildAt(int y) {
+        final int ix = (y - mTop) * mViews.size() / mHeight;
+        return ix;
+    }
+
+}
diff --git a/src/com/android/browser/view/PieMenu.java b/src/com/android/browser/view/PieMenu.java
new file mode 100644
index 0000000..1699c27
--- /dev/null
+++ b/src/com/android/browser/view/PieMenu.java
@@ -0,0 +1,636 @@
+/*
+ * Copyright (C) 2010 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.view;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Point;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+
+import com.android.browser.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PieMenu extends FrameLayout {
+
+    private static final int MAX_LEVELS = 5;
+    private static final long ANIMATION = 80;
+
+    public interface PieController {
+        /**
+         * called before menu opens to customize menu
+         * returns if pie state has been changed
+         */
+        public boolean onOpen();
+        public void stopEditingUrl();
+
+    }
+
+    /**
+     * A view like object that lives off of the pie menu
+     */
+    public interface PieView {
+
+        public interface OnLayoutListener {
+            public void onLayout(int ax, int ay, boolean left);
+        }
+
+        public void setLayoutListener(OnLayoutListener l);
+
+        public void layout(int anchorX, int anchorY, boolean onleft, float angle,
+                int parentHeight);
+
+        public void draw(Canvas c);
+
+        public boolean onTouchEvent(MotionEvent evt);
+
+    }
+
+    private Point mCenter;
+    private int mRadius;
+    private int mRadiusInc;
+    private int mSlop;
+    private int mTouchOffset;
+    private Path mPath;
+
+    private boolean mOpen;
+    private PieController mController;
+
+    private List<PieItem> mItems;
+    private int mLevels;
+    private int[] mCounts;
+    private PieView mPieView = null;
+
+    // sub menus
+    private List<PieItem> mCurrentItems;
+    private PieItem mOpenItem;
+
+    private Drawable mBackground;
+    private Paint mNormalPaint;
+    private Paint mSelectedPaint;
+    private Paint mSubPaint;
+
+    // touch handling
+    private PieItem mCurrentItem;
+
+    private boolean mUseBackground;
+    private boolean mAnimating;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public PieMenu(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(context);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public PieMenu(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(context);
+    }
+
+    /**
+     * @param context
+     */
+    public PieMenu(Context context) {
+        super(context);
+        init(context);
+    }
+
+    private void init(Context ctx) {
+        mItems = new ArrayList<PieItem>();
+        mLevels = 0;
+        mCounts = new int[MAX_LEVELS];
+        Resources res = ctx.getResources();
+        mRadius = (int) res.getDimension(R.dimen.qc_radius_start);
+        mRadiusInc = (int) res.getDimension(R.dimen.qc_radius_increment);
+        mSlop = (int) res.getDimension(R.dimen.qc_slop);
+        mTouchOffset = (int) res.getDimension(R.dimen.qc_touch_offset);
+        mOpen = false;
+        setWillNotDraw(false);
+        setDrawingCacheEnabled(false);
+        mCenter = new Point(0,0);
+        mBackground = res.getDrawable(R.drawable.qc_background_normal);
+        mNormalPaint = new Paint();
+        mNormalPaint.setColor(res.getColor(R.color.qc_normal));
+        mNormalPaint.setAntiAlias(true);
+        mSelectedPaint = new Paint();
+        mSelectedPaint.setColor(res.getColor(R.color.qc_selected));
+        mSelectedPaint.setAntiAlias(true);
+        mSubPaint = new Paint();
+        mSubPaint.setAntiAlias(true);
+        mSubPaint.setColor(res.getColor(R.color.qc_sub));
+    }
+
+    public void setController(PieController ctl) {
+        mController = ctl;
+    }
+
+    public void setUseBackground(boolean useBackground) {
+        mUseBackground = useBackground;
+    }
+
+    public void addItem(PieItem item) {
+        // add the item to the pie itself
+        mItems.add(item);
+        int l = item.getLevel();
+        mLevels = Math.max(mLevels, l);
+        mCounts[l]++;
+    }
+
+    public void removeItem(PieItem item) {
+        mItems.remove(item);
+    }
+
+    public void clearItems() {
+        mItems.clear();
+    }
+
+    private boolean onTheLeft() {
+        return mCenter.x < mSlop;
+    }
+
+    /**
+     * guaranteed has center set
+     * @param show
+     */
+    private void show(boolean show) {
+        mOpen = show;
+        if (mOpen) {
+            // ensure clean state
+            mAnimating = false;
+            mCurrentItem = null;
+            mOpenItem = null;
+            mPieView = null;
+            mController.stopEditingUrl();
+            mCurrentItems = mItems;
+            for (PieItem item : mCurrentItems) {
+                item.setSelected(false);
+            }
+            if (mController != null) {
+                boolean changed = mController.onOpen();
+            }
+            layoutPie();
+            animateOpen();
+        }
+        invalidate();
+    }
+
+    private void animateOpen() {
+        ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+        anim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                for (PieItem item : mCurrentItems) {
+                    item.setAnimationAngle((1 - animation.getAnimatedFraction()) * (- item.getStart()));
+                }
+                invalidate();
+            }
+
+        });
+        anim.setDuration(2*ANIMATION);
+        anim.start();
+    }
+
+    private void setCenter(int x, int y) {
+        if (x < mSlop) {
+            mCenter.x = 0;
+        } else {
+            mCenter.x = getWidth();
+        }
+        mCenter.y = y;
+    }
+
+    private void layoutPie() {
+        float emptyangle = (float) Math.PI / 16;
+        int rgap = 2;
+        int inner = mRadius + rgap;
+        int outer = mRadius + mRadiusInc - rgap;
+        int gap = 1;
+        for (int i = 0; i < mLevels; i++) {
+            int level = i + 1;
+            float sweep = (float) (Math.PI - 2 * emptyangle) / mCounts[level];
+            float angle = emptyangle + sweep / 2;
+            mPath = makeSlice(getDegrees(0) - gap, getDegrees(sweep) + gap, outer, inner, mCenter);
+            for (PieItem item : mCurrentItems) {
+                if (item.getLevel() == level) {
+                    View view = item.getView();
+                    if (view != null) {
+                        view.measure(view.getLayoutParams().width,
+                                view.getLayoutParams().height);
+                        int w = view.getMeasuredWidth();
+                        int h = view.getMeasuredHeight();
+                        int r = inner + (outer - inner) * 2 / 3;
+                        int x = (int) (r * Math.sin(angle));
+                        int y = mCenter.y - (int) (r * Math.cos(angle)) - h / 2;
+                        if (onTheLeft()) {
+                            x = mCenter.x + x - w / 2;
+                        } else {
+                            x = mCenter.x - x - w / 2;
+                        }
+                        view.layout(x, y, x + w, y + h);
+                    }
+                    float itemstart = angle - sweep / 2;
+                    item.setGeometry(itemstart, sweep, inner, outer);
+                    angle += sweep;
+                }
+            }
+            inner += mRadiusInc;
+            outer += mRadiusInc;
+        }
+    }
+
+
+    /**
+     * converts a
+     *
+     * @param angle from 0..PI to Android degrees (clockwise starting at 3
+     *        o'clock)
+     * @return skia angle
+     */
+    private float getDegrees(double angle) {
+        return (float) (270 - 180 * angle / Math.PI);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mOpen) {
+            int state;
+            if (mUseBackground) {
+                int w = mBackground.getIntrinsicWidth();
+                int h = mBackground.getIntrinsicHeight();
+                int left = mCenter.x - w;
+                int top = mCenter.y - h / 2;
+                mBackground.setBounds(left, top, left + w, top + h);
+                state = canvas.save();
+                if (onTheLeft()) {
+                    canvas.scale(-1, 1);
+                }
+                mBackground.draw(canvas);
+                canvas.restoreToCount(state);
+            }
+            // draw base menu
+            PieItem last = mCurrentItem;
+            if (mOpenItem != null) {
+                last = mOpenItem;
+            }
+            for (PieItem item : mCurrentItems) {
+                if (item != last) {
+                    drawItem(canvas, item);
+                }
+            }
+            if (last != null) {
+                drawItem(canvas, last);
+            }
+            if (mPieView != null) {
+                mPieView.draw(canvas);
+            }
+        }
+    }
+
+    private void drawItem(Canvas canvas, PieItem item) {
+        if (item.getView() != null) {
+            Paint p = item.isSelected() ? mSelectedPaint : mNormalPaint;
+            if (!mItems.contains(item)) {
+                p = item.isSelected() ? mSelectedPaint : mSubPaint;
+            }
+            int state = canvas.save();
+            if (onTheLeft()) {
+                canvas.scale(-1, 1);
+            }
+            float r = getDegrees(item.getStartAngle()) - 270; // degrees(0)
+            canvas.rotate(r, mCenter.x, mCenter.y);
+            canvas.drawPath(mPath, p);
+            canvas.restoreToCount(state);
+            // draw the item view
+            View view = item.getView();
+            state = canvas.save();
+            canvas.translate(view.getX(), view.getY());
+            view.draw(canvas);
+            canvas.restoreToCount(state);
+        }
+    }
+
+    private Path makeSlice(float start, float end, int outer, int inner, Point center) {
+        RectF bb =
+                new RectF(center.x - outer, center.y - outer, center.x + outer,
+                        center.y + outer);
+        RectF bbi =
+                new RectF(center.x - inner, center.y - inner, center.x + inner,
+                        center.y + inner);
+        Path path = new Path();
+        path.arcTo(bb, start, end - start, true);
+        path.arcTo(bbi, end, start - end);
+        path.close();
+        return path;
+    }
+
+    // touch handling for pie
+
+    @Override
+    public boolean onTouchEvent(MotionEvent evt) {
+        float x = evt.getX();
+        float y = evt.getY();
+        int action = evt.getActionMasked();
+        if (MotionEvent.ACTION_DOWN == action) {
+            if ((x > getWidth() - mSlop) || (x < mSlop)) {
+                setCenter((int) x, (int) y);
+                show(true);
+                return true;
+            }
+        } else if (MotionEvent.ACTION_UP == action) {
+            if (mOpen) {
+                boolean handled = false;
+                if (mPieView != null) {
+                    handled = mPieView.onTouchEvent(evt);
+                }
+                PieItem item = mCurrentItem;
+                if (!mAnimating) {
+                    deselect();
+                }
+                show(false);
+                if (!handled && (item != null) && (item.getView() != null)) {
+                    if ((item == mOpenItem) || !mAnimating) {
+                        item.getView().performClick();
+                    }
+                }
+                return true;
+            }
+        } else if (MotionEvent.ACTION_CANCEL == action) {
+            if (mOpen) {
+                show(false);
+            }
+            if (!mAnimating) {
+                deselect();
+                invalidate();
+            }
+            return false;
+        } else if (MotionEvent.ACTION_MOVE == action) {
+            if (mAnimating) return false;
+            boolean handled = false;
+            PointF polar = getPolar(x, y);
+            int maxr = mRadius + mLevels * mRadiusInc + 50;
+            if (mPieView != null) {
+                handled = mPieView.onTouchEvent(evt);
+            }
+            if (handled) {
+                invalidate();
+                return false;
+            }
+            if (polar.y < mRadius) {
+                if (mOpenItem != null) {
+                    closeSub();
+                } else if (!mAnimating) {
+                    deselect();
+                    invalidate();
+                }
+                return false;
+            }
+            if (polar.y > maxr) {
+                deselect();
+                show(false);
+                evt.setAction(MotionEvent.ACTION_DOWN);
+                if (getParent() != null) {
+                    ((ViewGroup) getParent()).dispatchTouchEvent(evt);
+                }
+                return false;
+            }
+            PieItem item = findItem(polar);
+            if (item == null) {
+            } else if (mCurrentItem != item) {
+                onEnter(item);
+                if ((item != null) && item.isPieView() && (item.getView() != null)) {
+                    int cx = item.getView().getLeft() + (onTheLeft()
+                            ? item.getView().getWidth() : 0);
+                    int cy = item.getView().getTop();
+                    mPieView = item.getPieView();
+                    layoutPieView(mPieView, cx, cy,
+                            (item.getStartAngle() + item.getSweep()) / 2);
+                }
+                invalidate();
+            }
+        }
+        // always re-dispatch event
+        return false;
+    }
+
+    private void layoutPieView(PieView pv, int x, int y, float angle) {
+        pv.layout(x, y, onTheLeft(), angle, getHeight());
+    }
+
+    /**
+     * enter a slice for a view
+     * updates model only
+     * @param item
+     */
+    private void onEnter(PieItem item) {
+        // deselect
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (item != null) {
+            // clear up stack
+            playSoundEffect(SoundEffectConstants.CLICK);
+            item.setSelected(true);
+            mPieView = null;
+            mCurrentItem = item;
+            if ((mCurrentItem != mOpenItem) && mCurrentItem.hasItems()) {
+                openSub(mCurrentItem);
+                mOpenItem = item;
+            }
+        } else {
+            mCurrentItem = null;
+        }
+
+    }
+
+    private void animateOut(final PieItem fixed, AnimatorListener listener) {
+        if ((mCurrentItems == null) || (fixed == null)) return;
+        final float target = fixed.getStartAngle();
+        ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+        anim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                for (PieItem item : mCurrentItems) {
+                    if (item != fixed) {
+                        item.setAnimationAngle(animation.getAnimatedFraction()
+                                * (target - item.getStart()));
+                    }
+                }
+                invalidate();
+            }
+        });
+        anim.setDuration(ANIMATION);
+        anim.addListener(listener);
+        anim.start();
+    }
+
+    private void animateIn(final PieItem fixed, AnimatorListener listener) {
+        if ((mCurrentItems == null) || (fixed == null)) return;
+        final float target = fixed.getStartAngle();
+        ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
+        anim.addUpdateListener(new AnimatorUpdateListener() {
+            @Override
+            public void onAnimationUpdate(ValueAnimator animation) {
+                for (PieItem item : mCurrentItems) {
+                    if (item != fixed) {
+                        item.setAnimationAngle((1 - animation.getAnimatedFraction())
+                                * (target - item.getStart()));
+                    }
+                }
+                invalidate();
+
+            }
+
+        });
+        anim.setDuration(ANIMATION);
+        anim.addListener(listener);
+        anim.start();
+    }
+
+    private void openSub(final PieItem item) {
+        mAnimating = true;
+        animateOut(item, new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator a) {
+                for (PieItem item : mCurrentItems) {
+                    item.setAnimationAngle(0);
+                }
+                mCurrentItems = new ArrayList<PieItem>(mItems.size());
+                int i = 0, j = 0;
+                while (i < mItems.size()) {
+                    if (mItems.get(i) == item) {
+                        mCurrentItems.add(item);
+                    } else {
+                        mCurrentItems.add(item.getItems().get(j++));
+                    }
+                    i++;
+                }
+                layoutPie();
+                animateIn(item, new AnimatorListenerAdapter() {
+                    public void onAnimationEnd(Animator a) {
+                        for (PieItem item : mCurrentItems) {
+                            item.setAnimationAngle(0);
+                        }
+                        mAnimating = false;
+                    }
+                });
+            }
+        });
+    }
+
+    private void closeSub() {
+        mAnimating = true;
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        animateOut(mOpenItem, new AnimatorListenerAdapter() {
+            public void onAnimationEnd(Animator a) {
+                for (PieItem item : mCurrentItems) {
+                    item.setAnimationAngle(0);
+                }
+                mCurrentItems = mItems;
+                mPieView = null;
+                animateIn(mOpenItem, new AnimatorListenerAdapter() {
+                    public void onAnimationEnd(Animator a) {
+                        for (PieItem item : mCurrentItems) {
+                            item.setAnimationAngle(0);
+                        }
+                        mAnimating = false;
+                        mOpenItem = null;
+                        mCurrentItem = null;
+                    }
+                });
+            }
+        });
+    }
+
+    private void deselect() {
+        if (mCurrentItem != null) {
+            mCurrentItem.setSelected(false);
+        }
+        if (mOpenItem != null) {
+            mOpenItem = null;
+            mCurrentItems = mItems;
+        }
+        mCurrentItem = null;
+        mPieView = null;
+    }
+
+    private PointF getPolar(float x, float y) {
+        PointF res = new PointF();
+        // get angle and radius from x/y
+        res.x = (float) Math.PI / 2;
+        x = mCenter.x - x;
+        if (mCenter.x < mSlop) {
+            x = -x;
+        }
+        y = mCenter.y - y;
+        res.y = (float) Math.sqrt(x * x + y * y);
+        if (y > 0) {
+            res.x = (float) Math.asin(x / res.y);
+        } else if (y < 0) {
+            res.x = (float) (Math.PI - Math.asin(x / res.y ));
+        }
+        return res;
+    }
+
+    /**
+     *
+     * @param polar x: angle, y: dist
+     * @return the item at angle/dist or null
+     */
+    private PieItem findItem(PointF polar) {
+        // find the matching item:
+        for (PieItem item : mCurrentItems) {
+            if (inside(polar, mTouchOffset, item)) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    private boolean inside(PointF polar, float offset, PieItem item) {
+        return (item.getInnerRadius() - offset < polar.y)
+        && (item.getOuterRadius() - offset > polar.y)
+        && (item.getStartAngle() < polar.x)
+        && (item.getStartAngle() + item.getSweep() > polar.x);
+    }
+
+}
diff --git a/src/com/android/browser/view/PieStackView.java b/src/com/android/browser/view/PieStackView.java
new file mode 100644
index 0000000..e1f41bd
--- /dev/null
+++ b/src/com/android/browser/view/PieStackView.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.view.View;
+
+/**
+ * shows views in a stack
+ */
+public class PieStackView extends BasePieView {
+
+    private static final int SLOP = 5;
+
+    private OnCurrentListener mCurrentListener;
+    private int mMinHeight;
+
+    public interface OnCurrentListener {
+        public void onSetCurrent(int index);
+    }
+
+    public PieStackView(Context ctx) {
+        mMinHeight = (int) ctx.getResources()
+                .getDimension(R.dimen.qc_tab_title_height);
+    }
+
+    public void setOnCurrentListener(OnCurrentListener l) {
+        mCurrentListener = l;
+    }
+
+    @Override
+    public void setCurrent(int ix) {
+        super.setCurrent(ix);
+        if (mCurrentListener != null) {
+            mCurrentListener.onSetCurrent(ix);
+        }
+    }
+
+    /**
+     * this will be called before the first draw call
+     */
+    @Override
+    public void layout(int anchorX, int anchorY, boolean left, float angle,
+            int pHeight) {
+        super.layout(anchorX, anchorY, left, angle, pHeight);
+        buildViews();
+        mWidth = mChildWidth;
+        mHeight = mChildHeight + (mViews.size() - 1) * mMinHeight;
+        mLeft = anchorX + (left ? SLOP : -(SLOP + mChildWidth));
+        mTop = anchorY - mHeight / 2;
+        if (mViews != null) {
+            layoutChildrenLinear();
+        }
+    }
+
+    private void layoutChildrenLinear() {
+        final int n = mViews.size();
+        int top = mTop;
+        int dy = (n == 1) ? 0 : (mHeight - mChildHeight) / (n - 1);
+        for (View view : mViews) {
+            int x = mLeft;
+            view.layout(x, top, x + mChildWidth, top + mChildHeight);
+            top += dy;
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        if ((mViews != null) && (mCurrent > -1)) {
+            final int n = mViews.size();
+            for (int i = 0; i < mCurrent; i++) {
+                drawView(mViews.get(i), canvas);
+            }
+            for (int i = n - 1; i > mCurrent; i--) {
+                drawView(mViews.get(i), canvas);
+            }
+            drawView(mViews.get(mCurrent), canvas);
+        }
+    }
+
+    @Override
+    protected int findChildAt(int y) {
+        final int ix = (y - mTop) * mViews.size() / mHeight;
+        return ix;
+    }
+
+}
diff --git a/src/com/android/browser/view/ScrollerView.java b/src/com/android/browser/view/ScrollerView.java
new file mode 100644
index 0000000..7e5a4c8
--- /dev/null
+++ b/src/com/android/browser/view/ScrollerView.java
@@ -0,0 +1,1952 @@
+/*
+ * Copyright (C) 2006 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.view;
+
+import android.content.Context;
+import android.graphics.PointF;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.OverScroller;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Layout container for a view hierarchy that can be scrolled by the user,
+ * allowing it to be larger than the physical display.  A ScrollView
+ * is a {@link FrameLayout}, meaning you should place one child in it
+ * containing the entire contents to scroll; this child may itself be a layout
+ * manager with a complex hierarchy of objects.  A child that is often used
+ * is a {@link LinearLayout} in a vertical orientation, presenting a vertical
+ * array of top-level items that the user can scroll through.
+ *
+ * <p>The {@link TextView} class also
+ * takes care of its own scrolling, so does not require a ScrollView, but
+ * using the two together is possible to achieve the effect of a text view
+ * within a larger container.
+ *
+ * <p>ScrollView only supports vertical scrolling.
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+public class ScrollerView extends FrameLayout {
+    static final int ANIMATED_SCROLL_GAP = 250;
+
+    static final float MAX_SCROLL_FACTOR = 0.5f;
+
+    private long mLastScroll;
+
+    private final Rect mTempRect = new Rect();
+    protected OverScroller mScroller;
+
+    /**
+     * Position of the last motion event.
+     */
+    private float mLastMotionY;
+
+    /**
+     * True when the layout has changed but the traversal has not come through yet.
+     * Ideally the view hierarchy would keep track of this for us.
+     */
+    private boolean mIsLayoutDirty = true;
+
+    /**
+     * The child to give focus to in the event that a child has requested focus while the
+     * layout is dirty. This prevents the scroll from being wrong if the child has not been
+     * laid out before requesting focus.
+     */
+    protected View mChildToScrollTo = null;
+
+    /**
+     * True if the user is currently dragging this ScrollView around. This is
+     * not the same as 'is being flinged', which can be checked by
+     * mScroller.isFinished() (flinging begins when the user lifts his finger).
+     */
+    protected boolean mIsBeingDragged = false;
+
+    /**
+     * Determines speed during touch scrolling
+     */
+    private VelocityTracker mVelocityTracker;
+
+    /**
+     * When set to true, the scroll view measure its child to make it fill the currently
+     * visible area.
+     */
+    @ViewDebug.ExportedProperty(category = "layout")
+    private boolean mFillViewport;
+
+    /**
+     * Whether arrow scrolling is animated.
+     */
+    private boolean mSmoothScrollingEnabled = true;
+
+    private int mTouchSlop;
+    protected int mMinimumVelocity;
+    private int mMaximumVelocity;
+
+    private int mOverscrollDistance;
+    private int mOverflingDistance;
+
+    /**
+     * ID of the active pointer. This is used to retain consistency during
+     * drags/flings if multiple pointers are used.
+     */
+    private int mActivePointerId = INVALID_POINTER;
+
+    private static class ThreadSpanState {
+        public Span mActiveHead;    // doubly-linked list.
+        public int mActiveSize;
+        public Span mFreeListHead;  // singly-linked list.  only changes at head.
+        public int mFreeListSize;
+    }
+
+    public static class Span {
+        private String mName;
+        private long mCreateMillis;
+        private Span mNext;
+        private Span mPrev;  // not used when in freeList, only active
+        private final ThreadSpanState mContainerState;
+
+        Span(ThreadSpanState threadState) {
+            mContainerState = threadState;
+        }
+
+        // Empty constructor for the NO_OP_SPAN
+        protected Span() {
+            mContainerState = null;
+        }
+
+        /**
+         * To be called when the critical span is complete (i.e. the
+         * animation is done animating).  This can be called on any
+         * thread (even a different one from where the animation was
+         * taking place), but that's only a defensive implementation
+         * measure.  It really makes no sense for you to call this on
+         * thread other than that where you created it.
+         *
+         * @hide
+         */
+        public void finish() {
+            ThreadSpanState state = mContainerState;
+            synchronized (state) {
+                if (mName == null) {
+                    // Duplicate finish call.  Ignore.
+                    return;
+                }
+
+                // Remove ourselves from the active list.
+                if (mPrev != null) {
+                    mPrev.mNext = mNext;
+                }
+                if (mNext != null) {
+                    mNext.mPrev = mPrev;
+                }
+                if (state.mActiveHead == this) {
+                    state.mActiveHead = mNext;
+                }
+
+                state.mActiveSize--;
+
+                this.mCreateMillis = -1;
+                this.mName = null;
+                this.mPrev = null;
+                this.mNext = null;
+
+                // Add ourselves to the freeList, if it's not already
+                // too big.
+                if (state.mFreeListSize < 5) {
+                    this.mNext = state.mFreeListHead;
+                    state.mFreeListHead = this;
+                    state.mFreeListSize++;
+                }
+            }
+        }
+    }
+
+    private static final Span NO_OP_SPAN = new Span() {
+        public void finish() {
+            // Do nothing.
+        }
+    };
+
+    /**
+     * The StrictMode "critical time span" objects to catch animation
+     * stutters.  Non-null when a time-sensitive animation is
+     * in-flight.  Must call finish() on them when done animating.
+     * These are no-ops on user builds.
+     */
+    private Span mScrollStrictSpan = null;  // aka "drag"
+    private Span mFlingStrictSpan = null;
+
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    /**
+     * orientation of the scrollview
+     */
+    protected boolean mHorizontal;
+
+    protected boolean mIsOrthoDragged;
+    private float mLastOrthoCoord;
+    private View mDownView;
+    private PointF mDownCoords;
+
+
+    public ScrollerView(Context context) {
+        this(context, null);
+    }
+
+    public ScrollerView(Context context, AttributeSet attrs) {
+        this(context, attrs, android.R.attr.scrollViewStyle);
+    }
+
+    public ScrollerView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        initScrollView();
+        // SWE_TODO : Fix me
+        /*
+        TypedArray a =
+            context.obtainStyledAttributes(attrs, R.styleable.ScrollView, defStyle, 0);
+        setFillViewport(a.getBoolean(R.styleable.ScrollView_android_fillViewport, false));
+        a.recycle();*/
+        setFillViewport(false);
+    }
+
+    private void initScrollView() {
+        mScroller = new OverScroller(getContext());
+        setFocusable(true);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setWillNotDraw(false);
+        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mOverscrollDistance = configuration.getScaledOverscrollDistance();
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+        mDownCoords = new PointF();
+    }
+
+    public void setOrientation(int orientation) {
+        mHorizontal = (orientation == LinearLayout.HORIZONTAL);
+        requestLayout();
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    @Override
+    protected float getTopFadingEdgeStrength() {
+        if (getChildCount() == 0) {
+            return 0.0f;
+        }
+        if (mHorizontal) {
+            final int length = getHorizontalFadingEdgeLength();
+            if (getScrollX() < length) {
+                return getScrollX() / (float) length;
+            }
+        } else {
+            final int length = getVerticalFadingEdgeLength();
+            if (getScrollY() < length) {
+                return getScrollY() / (float) length;
+            }
+        }
+        return 1.0f;
+    }
+
+    @Override
+    protected float getBottomFadingEdgeStrength() {
+        if (getChildCount() == 0) {
+            return 0.0f;
+        }
+        if (mHorizontal) {
+            final int length = getHorizontalFadingEdgeLength();
+            final int bottomEdge = getWidth() - getPaddingRight();
+            final int span = getChildAt(0).getRight() - getScrollX() - bottomEdge;
+            if (span < length) {
+                return span / (float) length;
+            }
+        } else {
+            final int length = getVerticalFadingEdgeLength();
+            final int bottomEdge = getHeight() - getPaddingBottom();
+            final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge;
+            if (span < length) {
+                return span / (float) length;
+            }
+        }
+        return 1.0f;
+    }
+
+    /**
+     * @return The maximum amount this scroll view will scroll in response to
+     *   an arrow event.
+     */
+    public int getMaxScrollAmount() {
+        return (int) (MAX_SCROLL_FACTOR * (mHorizontal
+                ? (getRight() - getLeft()) : (getBottom() - getTop())));
+    }
+
+
+    @Override
+    public void addView(View child) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child);
+    }
+
+    @Override
+    public void addView(View child, int index) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child, index);
+    }
+
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child, params);
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child, index, params);
+    }
+
+    /**
+     * @return Returns true this ScrollView can be scrolled
+     */
+    private boolean canScroll() {
+        View child = getChildAt(0);
+        if (child != null) {
+            if (mHorizontal) {
+                return getWidth() < child.getWidth() + getPaddingLeft() + getPaddingRight();
+            } else {
+                return getHeight() < child.getHeight() + getPaddingTop() + getPaddingBottom();
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this ScrollView's content is stretched to fill the viewport.
+     *
+     * @return True if the content fills the viewport, false otherwise.
+     *
+     * @attr ref android.R.styleable#ScrollView_fillViewport
+     */
+    public boolean isFillViewport() {
+        return mFillViewport;
+    }
+
+    /**
+     * Indicates this ScrollView whether it should stretch its content height to fill
+     * the viewport or not.
+     *
+     * @param fillViewport True to stretch the content's height to the viewport's
+     *        boundaries, false otherwise.
+     *
+     * @attr ref android.R.styleable#ScrollView_fillViewport
+     */
+    public void setFillViewport(boolean fillViewport) {
+        if (fillViewport != mFillViewport) {
+            mFillViewport = fillViewport;
+            requestLayout();
+        }
+    }
+
+    /**
+     * @return Whether arrow scrolling will animate its transition.
+     */
+    public boolean isSmoothScrollingEnabled() {
+        return mSmoothScrollingEnabled;
+    }
+
+    /**
+     * Set whether arrow scrolling will animate its transition.
+     * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
+     */
+    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
+        mSmoothScrollingEnabled = smoothScrollingEnabled;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (!mFillViewport) {
+            return;
+        }
+
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (heightMode == MeasureSpec.UNSPECIFIED) {
+            return;
+        }
+
+        if (getChildCount() > 0) {
+            final View child = getChildAt(0);
+            if (mHorizontal) {
+                int width = getMeasuredWidth();
+                if (child.getMeasuredWidth() < width) {
+                    final FrameLayout.LayoutParams lp = (LayoutParams) child
+                            .getLayoutParams();
+
+                    int childHeightMeasureSpec = getChildMeasureSpec(
+                            heightMeasureSpec, getPaddingTop() + getPaddingBottom(),
+                            lp.height);
+                    width -= getPaddingLeft();
+                    width -= getPaddingRight();
+                    int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            width, MeasureSpec.EXACTLY);
+
+                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+                }
+            } else {
+                int height = getMeasuredHeight();
+                if (child.getMeasuredHeight() < height) {
+                    final FrameLayout.LayoutParams lp = (LayoutParams) child
+                            .getLayoutParams();
+
+                    int childWidthMeasureSpec = getChildMeasureSpec(
+                            widthMeasureSpec, getPaddingLeft() + getPaddingRight(),
+                            lp.width);
+                    height -= getPaddingTop();
+                    height -= getPaddingBottom();
+                    int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            height, MeasureSpec.EXACTLY);
+
+                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Let the focused view and/or our descendants get the key first
+        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+    }
+
+    /**
+     * You can call this function yourself to have the scroll view perform
+     * scrolling from a key event, just as if the event had been dispatched to
+     * it by the view hierarchy.
+     *
+     * @param event The key event to execute.
+     * @return Return true if the event was handled, else false.
+     */
+    public boolean executeKeyEvent(KeyEvent event) {
+        mTempRect.setEmpty();
+
+        if (!canScroll()) {
+            if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
+                View currentFocused = findFocus();
+                if (currentFocused == this) currentFocused = null;
+                View nextFocused = FocusFinder.getInstance().findNextFocus(this,
+                        currentFocused, View.FOCUS_DOWN);
+                return nextFocused != null
+                        && nextFocused != this
+                        && nextFocused.requestFocus(View.FOCUS_DOWN);
+            }
+            return false;
+        }
+
+        boolean handled = false;
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    if (!event.isAltPressed()) {
+                        handled = arrowScroll(View.FOCUS_UP);
+                    } else {
+                        handled = fullScroll(View.FOCUS_UP);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    if (!event.isAltPressed()) {
+                        handled = arrowScroll(View.FOCUS_DOWN);
+                    } else {
+                        handled = fullScroll(View.FOCUS_DOWN);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_SPACE:
+                    pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
+                    break;
+            }
+        }
+
+        return handled;
+    }
+
+    private boolean inChild(int x, int y) {
+        if (getChildCount() > 0) {
+            final int scrollY = getScrollY();
+            final View child = getChildAt(0);
+            return !(y < child.getTop() - scrollY
+                    || y >= child.getBottom() - scrollY
+                    || x < child.getLeft()
+                    || x >= child.getRight());
+        }
+        return false;
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (disallowIntercept) {
+            recycleVelocityTracker();
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        /*
+         * Shortcut the most recurring case: the user is in the dragging state
+         * and he is moving his finger. We want to intercept this motion.
+         */
+        final int action = ev.getAction();
+        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+            return true;
+        }
+        if ((action == MotionEvent.ACTION_MOVE) && (mIsOrthoDragged)) {
+            return true;
+        }
+        switch (action & MotionEvent.ACTION_MASK) {
+        case MotionEvent.ACTION_MOVE: {
+            /*
+             * mIsBeingDragged == false, otherwise the shortcut would have
+             * caught it. Check whether the user has moved far enough from his
+             * original down touch.
+             */
+
+            /*
+             * Locally do absolute value. mLastMotionY is set to the y value of
+             * the down event.
+             */
+            final int activePointerId = mActivePointerId;
+            if (activePointerId == INVALID_POINTER) {
+                // If we don't have a valid id, the touch down wasn't on
+                // content.
+                break;
+            }
+
+            final int pointerIndex = ev.findPointerIndex(activePointerId);
+            final float y = mHorizontal ? ev.getX(pointerIndex) : ev
+                    .getY(pointerIndex);
+            final int yDiff = (int) Math.abs(y - mLastMotionY);
+            if (yDiff > mTouchSlop) {
+                mIsBeingDragged = true;
+                mLastMotionY = y;
+                initVelocityTrackerIfNotExists();
+                mVelocityTracker.addMovement(ev);
+                if (mScrollStrictSpan == null) {
+                    /*mScrollStrictSpan = StrictMode
+                            .enterCriticalSpan("ScrollView-scroll");*/
+                    mScrollStrictSpan = NO_OP_SPAN;
+                }
+            } else {
+                final float ocoord = mHorizontal ? ev.getY(pointerIndex) : ev
+                        .getX(pointerIndex);
+                if (Math.abs(ocoord - mLastOrthoCoord) > mTouchSlop) {
+                    mIsOrthoDragged = true;
+                    mLastOrthoCoord = ocoord;
+                    initVelocityTrackerIfNotExists();
+                    mVelocityTracker.addMovement(ev);
+                }
+            }
+            break;
+        }
+
+        case MotionEvent.ACTION_DOWN: {
+            final float y = mHorizontal ? ev.getX() : ev.getY();
+            mDownCoords.x = ev.getX();
+            mDownCoords.y = ev.getY();
+            if (!inChild((int) ev.getX(), (int) ev.getY())) {
+                mIsBeingDragged = false;
+                recycleVelocityTracker();
+                break;
+            }
+
+            /*
+             * Remember location of down touch. ACTION_DOWN always refers to
+             * pointer index 0.
+             */
+            mLastMotionY = y;
+            mActivePointerId = ev.getPointerId(0);
+
+            initOrResetVelocityTracker();
+            mVelocityTracker.addMovement(ev);
+            /*
+             * If being flinged and user touches the screen, initiate drag;
+             * otherwise don't. mScroller.isFinished should be false when being
+             * flinged.
+             */
+            mIsBeingDragged = !mScroller.isFinished();
+            if (mIsBeingDragged && mScrollStrictSpan == null) {
+                /*mScrollStrictSpan = StrictMode
+                        .enterCriticalSpan("ScrollView-scroll");*/
+                mScrollStrictSpan = NO_OP_SPAN;
+            }
+            mIsOrthoDragged = false;
+            final float ocoord = mHorizontal ? ev.getY() : ev.getX();
+            mLastOrthoCoord = ocoord;
+            mDownView = findViewAt((int) ev.getX(), (int) ev.getY());
+            break;
+        }
+
+        case MotionEvent.ACTION_CANCEL:
+        case MotionEvent.ACTION_UP:
+            /* Release the drag */
+            mIsBeingDragged = false;
+            mIsOrthoDragged = false;
+            mActivePointerId = INVALID_POINTER;
+            recycleVelocityTracker();
+            if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
+                    getScrollRange())) {
+                invalidate();
+            }
+            break;
+        case MotionEvent.ACTION_POINTER_UP:
+            onSecondaryPointerUp(ev);
+            break;
+        }
+
+        /*
+         * The only time we want to intercept motion events is if we are in the
+         * drag mode.
+         */
+        return mIsBeingDragged || mIsOrthoDragged;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        initVelocityTrackerIfNotExists();
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                mIsBeingDragged = getChildCount() != 0;
+                if (!mIsBeingDragged) {
+                    return false;
+                }
+
+                /*
+                 * If being flinged and user touches, stop the fling. isFinished
+                 * will be false if being flinged.
+                 */
+                if (!mScroller.isFinished()) {
+                    mScroller.abortAnimation();
+                    if (mFlingStrictSpan != null) {
+                        mFlingStrictSpan.finish();
+                        mFlingStrictSpan = null;
+                    }
+                }
+
+                // Remember where the motion event started
+                mLastMotionY = mHorizontal ? ev.getX() : ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                if (mIsOrthoDragged) {
+                    // Scroll to follow the motion event
+                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                    final float x = ev.getX(activePointerIndex);
+                    final float y = ev.getY(activePointerIndex);
+                    if (isOrthoMove(x - mDownCoords.x, y - mDownCoords.y)) {
+                        onOrthoDrag(mDownView, mHorizontal
+                                ? y - mDownCoords.y
+                                : x - mDownCoords.x);
+                    }
+                } else if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                    final float y = mHorizontal ? ev.getX(activePointerIndex)
+                            : ev.getY(activePointerIndex);
+                    final int deltaY = (int) (mLastMotionY - y);
+                    mLastMotionY = y;
+
+                    final int oldX = getScrollX();
+                    final int oldY = getScrollY();
+                    final int range = getScrollRange();
+                    if (mHorizontal) {
+                        if (overScrollBy(deltaY, 0, getScrollX(), 0, range, 0,
+                                mOverscrollDistance, 0, true)) {
+                            // Break our velocity if we hit a scroll barrier.
+                            mVelocityTracker.clear();
+                        }
+                    } else {
+                        if (overScrollBy(0, deltaY, 0, getScrollY(), 0, range,
+                                0, mOverscrollDistance, true)) {
+                            // Break our velocity if we hit a scroll barrier.
+                            mVelocityTracker.clear();
+                        }
+                    }
+                    onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
+
+                    final int overscrollMode = getOverScrollMode();
+                    if (overscrollMode == OVER_SCROLL_ALWAYS ||
+                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0)) {
+                        final int pulledToY = mHorizontal ? oldX + deltaY : oldY + deltaY;
+                        if (pulledToY < 0) {
+                            onPull(pulledToY);
+                        } else if (pulledToY > range) {
+                            onPull(pulledToY - range);
+                        } else {
+                            onPull(0);
+                        }
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                final VelocityTracker vtracker = mVelocityTracker;
+                vtracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                if (isOrthoMove(vtracker.getXVelocity(mActivePointerId),
+                        vtracker.getYVelocity(mActivePointerId))
+                        && mMinimumVelocity < Math.abs((mHorizontal ? vtracker.getYVelocity()
+                                : vtracker.getXVelocity()))) {
+                    onOrthoFling(mDownView, mHorizontal ? vtracker.getYVelocity()
+                            : vtracker.getXVelocity());
+                    break;
+                }
+                if (mIsOrthoDragged) {
+                    onOrthoDragFinished(mDownView);
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                } else if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = mHorizontal
+                            ? (int) velocityTracker.getXVelocity(mActivePointerId)
+                            : (int) velocityTracker.getYVelocity(mActivePointerId);
+
+                    if (getChildCount() > 0) {
+                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                            fling(-initialVelocity);
+                        } else {
+                            final int bottom = getScrollRange();
+                            if (mHorizontal) {
+                                if (mScroller.springBack(getScrollX(), getScrollY(), 0,
+                                                         bottom, 0, 0)) {
+                                    invalidate();
+                                }
+                            } else {
+                                if (mScroller.springBack(getScrollX(), getScrollY(), 0,
+                                                         0, 0, bottom)) {
+                                    invalidate();
+                                }
+                            }
+                        }
+                        onPull(0);
+                    }
+
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsOrthoDragged) {
+                    onOrthoDragFinished(mDownView);
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                } else if (mIsBeingDragged && getChildCount() > 0) {
+                    if (mHorizontal) {
+                        if (mScroller.springBack(getScrollX(), getScrollY(), 0,
+                                                 getScrollRange(), 0, 0)) {
+                            invalidate();
+                        }
+                    } else {
+                        if (mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0,
+                                                 getScrollRange())) {
+                            invalidate();
+                        }
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                final float y = mHorizontal ? ev.getX(index) : ev.getY(index);
+                mLastMotionY = y;
+                mLastOrthoCoord = mHorizontal ? ev.getY(index) : ev.getX(index);
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionY = mHorizontal
+                        ? ev.getX(ev.findPointerIndex(mActivePointerId))
+                        : ev.getY(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+        return true;
+    }
+
+    protected View findViewAt(int x, int y) {
+        // subclass responsibility
+        return null;
+    }
+
+    protected void onPull(int delta) {
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionY = mHorizontal ? ev.getX(newPointerIndex) : ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+            mLastOrthoCoord = mHorizontal ? ev.getY(newPointerIndex)
+                    : ev.getX(newPointerIndex);
+        }
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+            case MotionEvent.ACTION_SCROLL: {
+                if (!mIsBeingDragged) {
+                    if (mHorizontal) {
+                        final float hscroll = event
+                                .getAxisValue(MotionEvent.AXIS_HSCROLL);
+                        if (hscroll != 0) {
+                            /* SWE_TODO : - disruptive getHorizontalScrollFactor()*/
+                            final int delta = (int) (hscroll * 10);
+                            final int range = getScrollRange();
+                            int oldScrollX = getScrollX();
+                            int newScrollX = oldScrollX - delta;
+                            if (newScrollX < 0) {
+                                newScrollX = 0;
+                            } else if (newScrollX > range) {
+                                newScrollX = range;
+                            }
+                            if (newScrollX != oldScrollX) {
+                                super.scrollTo(newScrollX, getScrollY());
+                                return true;
+                            }
+                        }
+                    } else {
+                        final float vscroll = event
+                                .getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        if (vscroll != 0) {
+                            /* SWE_TODO : - disruptive getVerticalScrollFactor()*/
+                            final int delta = (int) (vscroll * 10);
+                            final int range = getScrollRange();
+                            int oldScrollY = getScrollY();
+                            int newScrollY = oldScrollY - delta;
+                            if (newScrollY < 0) {
+                                newScrollY = 0;
+                            } else if (newScrollY > range) {
+                                newScrollY = range;
+                            }
+                            if (newScrollY != oldScrollY) {
+                                super.scrollTo(getScrollX(), newScrollY);
+                                return true;
+                            }
+                        }
+                    }
+                }
+            }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    protected void onOrthoDrag(View draggedView, float distance) {
+    }
+
+    protected void onOrthoDragFinished(View draggedView) {
+    }
+
+    protected void onOrthoFling(View draggedView, float velocity) {
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY,
+            boolean clampedX, boolean clampedY) {
+        // Treat animating scrolls differently; see #computeScroll() for why.
+        if (!mScroller.isFinished()) {
+            setScrollX(scrollX);
+            setScrollY(scrollY);
+            if (isHardwareAccelerated() && getParent() instanceof View) {
+                ((View) getParent()).invalidate();
+            }
+            if (mHorizontal && clampedX) {
+                mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRange(), 0, 0);
+            } else if (!mHorizontal && clampedY) {
+                mScroller.springBack(getScrollX(), getScrollY(), 0, 0, 0, getScrollRange());
+            }
+        } else {
+            super.scrollTo(scrollX, scrollY);
+        }
+        awakenScrollBars();
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfo(info);
+        info.setScrollable(true);
+    }
+
+    @Override
+    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEvent(event);
+        event.setScrollable(true);
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        // Do not append text content to scroll events they are fired frequently
+        // and the client has already received another event type with the text.
+        if (event.getEventType() != AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+            super.dispatchPopulateAccessibilityEvent(event);
+        }
+        return false;
+    }
+
+    private int getScrollRange() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            if (mHorizontal) {
+                scrollRange = Math.max(0,
+                        child.getWidth() - (getWidth() - getPaddingRight() - getPaddingLeft()));
+            } else {
+                scrollRange = Math.max(0,
+                        child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop()));
+            }
+        }
+        return scrollRange;
+    }
+
+    /**
+     * <p>
+     * Finds the next focusable component that fits in this View's bounds
+     * (excluding fading edges) pretending that this View's top is located at
+     * the parameter top.
+     * </p>
+     *
+     * @param topFocus           look for a candidate at the top of the bounds if topFocus is true,
+     *                           or at the bottom of the bounds if topFocus is false
+     * @param top                the top offset of the bounds in which a focusable must be
+     *                           found (the fading edge is assumed to start at this position)
+     * @param preferredFocusable the View that has highest priority and will be
+     *                           returned if it is within my bounds (null is valid)
+     * @return the next focusable component in the bounds or null if none can be found
+     */
+    private View findFocusableViewInMyBounds(final boolean topFocus,
+            final int top, View preferredFocusable) {
+        /*
+         * The fading edge's transparent side should be considered for focus
+         * since it's mostly visible, so we divide the actual fading edge length
+         * by 2.
+         */
+        final int fadingEdgeLength = (mHorizontal
+                ? getHorizontalFadingEdgeLength()
+                : getVerticalFadingEdgeLength()) / 2;
+        final int topWithoutFadingEdge = top + fadingEdgeLength;
+        final int bottomWithoutFadingEdge = top + (mHorizontal ? getWidth() : getHeight()) - fadingEdgeLength;
+
+        if ((preferredFocusable != null)
+                && ((mHorizontal ? preferredFocusable.getLeft() : preferredFocusable.getTop())
+                        < bottomWithoutFadingEdge)
+                && ((mHorizontal ? preferredFocusable.getRight() : preferredFocusable.getBottom()) > topWithoutFadingEdge)) {
+            return preferredFocusable;
+        }
+
+        return findFocusableViewInBounds(topFocus, topWithoutFadingEdge,
+                bottomWithoutFadingEdge);
+    }
+
+    /**
+     * <p>
+     * Finds the next focusable component that fits in the specified bounds.
+     * </p>
+     *
+     * @param topFocus look for a candidate is the one at the top of the bounds
+     *                 if topFocus is true, or at the bottom of the bounds if topFocus is
+     *                 false
+     * @param top      the top offset of the bounds in which a focusable must be
+     *                 found
+     * @param bottom   the bottom offset of the bounds in which a focusable must
+     *                 be found
+     * @return the next focusable component in the bounds or null if none can
+     *         be found
+     */
+    private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
+
+        List<View> focusables = getFocusables(View.FOCUS_FORWARD);
+        View focusCandidate = null;
+
+        /*
+         * A fully contained focusable is one where its top is below the bound's
+         * top, and its bottom is above the bound's bottom. A partially
+         * contained focusable is one where some part of it is within the
+         * bounds, but it also has some part that is not within bounds.  A fully contained
+         * focusable is preferred to a partially contained focusable.
+         */
+        boolean foundFullyContainedFocusable = false;
+
+        int count = focusables.size();
+        for (int i = 0; i < count; i++) {
+            View view = focusables.get(i);
+            int viewTop = mHorizontal ? view.getLeft() : view.getTop();
+            int viewBottom = mHorizontal ? view.getRight() : view.getBottom();
+
+            if (top < viewBottom && viewTop < bottom) {
+                /*
+                 * the focusable is in the target area, it is a candidate for
+                 * focusing
+                 */
+
+                final boolean viewIsFullyContained = (top < viewTop) &&
+                        (viewBottom < bottom);
+
+                if (focusCandidate == null) {
+                    /* No candidate, take this one */
+                    focusCandidate = view;
+                    foundFullyContainedFocusable = viewIsFullyContained;
+                } else {
+                    final int ctop = mHorizontal ? focusCandidate.getLeft() : focusCandidate.getTop();
+                    final int cbot = mHorizontal ? focusCandidate.getRight() : focusCandidate.getBottom();
+                    final boolean viewIsCloserToBoundary =
+                            (topFocus && viewTop < ctop) ||
+                                    (!topFocus && viewBottom > cbot);
+
+                    if (foundFullyContainedFocusable) {
+                        if (viewIsFullyContained && viewIsCloserToBoundary) {
+                            /*
+                             * We're dealing with only fully contained views, so
+                             * it has to be closer to the boundary to beat our
+                             * candidate
+                             */
+                            focusCandidate = view;
+                        }
+                    } else {
+                        if (viewIsFullyContained) {
+                            /* Any fully contained view beats a partially contained view */
+                            focusCandidate = view;
+                            foundFullyContainedFocusable = true;
+                        } else if (viewIsCloserToBoundary) {
+                            /*
+                             * Partially contained view beats another partially
+                             * contained view if it's closer
+                             */
+                            focusCandidate = view;
+                        }
+                    }
+                }
+            }
+        }
+
+        return focusCandidate;
+    }
+
+    // i was here
+
+    /**
+     * <p>Handles scrolling in response to a "page up/down" shortcut press. This
+     * method will scroll the view by one page up or down and give the focus
+     * to the topmost/bottommost component in the new visible area. If no
+     * component is a good candidate for focus, this scrollview reclaims the
+     * focus.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+     *                  to go one page up or
+     *                  {@link android.view.View#FOCUS_DOWN} to go one page down
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    public boolean pageScroll(int direction) {
+        boolean down = direction == View.FOCUS_DOWN;
+        int height = getHeight();
+
+        if (down) {
+            mTempRect.top = getScrollY() + height;
+            int count = getChildCount();
+            if (count > 0) {
+                View view = getChildAt(count - 1);
+                if (mTempRect.top + height > view.getBottom()) {
+                    mTempRect.top = view.getBottom() - height;
+                }
+            }
+        } else {
+            mTempRect.top = getScrollY() - height;
+            if (mTempRect.top < 0) {
+                mTempRect.top = 0;
+            }
+        }
+        mTempRect.bottom = mTempRect.top + height;
+
+        return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+    }
+
+    /**
+     * <p>Handles scrolling in response to a "home/end" shortcut press. This
+     * method will scroll the view to the top or bottom and give the focus
+     * to the topmost/bottommost component in the new visible area. If no
+     * component is a good candidate for focus, this scrollview reclaims the
+     * focus.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+     *                  to go the top of the view or
+     *                  {@link android.view.View#FOCUS_DOWN} to go the bottom
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    public boolean fullScroll(int direction) {
+        boolean down = direction == View.FOCUS_DOWN;
+        int height = getHeight();
+
+        mTempRect.top = 0;
+        mTempRect.bottom = height;
+
+        if (down) {
+            int count = getChildCount();
+            if (count > 0) {
+                View view = getChildAt(count - 1);
+                mTempRect.bottom = view.getBottom() + getPaddingBottom();
+                mTempRect.top = mTempRect.bottom - height;
+            }
+        }
+
+        return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+    }
+
+    /**
+     * <p>Scrolls the view to make the area defined by <code>top</code> and
+     * <code>bottom</code> visible. This method attempts to give the focus
+     * to a component visible in this area. If no component can be focused in
+     * the new visible area, the focus is reclaimed by this ScrollView.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+     *                  to go upward, {@link android.view.View#FOCUS_DOWN} to downward
+     * @param top       the top offset of the new area to be made visible
+     * @param bottom    the bottom offset of the new area to be made visible
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    private boolean scrollAndFocus(int direction, int top, int bottom) {
+        boolean handled = true;
+
+        int height = getHeight();
+        int containerTop = getScrollY();
+        int containerBottom = containerTop + height;
+        boolean up = direction == View.FOCUS_UP;
+
+        View newFocused = findFocusableViewInBounds(up, top, bottom);
+        if (newFocused == null) {
+            newFocused = this;
+        }
+
+        if (top >= containerTop && bottom <= containerBottom) {
+            handled = false;
+        } else {
+            int delta = up ? (top - containerTop) : (bottom - containerBottom);
+            doScrollY(delta);
+        }
+
+        if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+        return handled;
+    }
+
+    /**
+     * Handle scrolling in response to an up or down arrow click.
+     *
+     * @param direction The direction corresponding to the arrow key that was
+     *                  pressed
+     * @return True if we consumed the event, false otherwise
+     */
+    public boolean arrowScroll(int direction) {
+
+        View currentFocused = findFocus();
+        if (currentFocused == this) currentFocused = null;
+
+        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+        final int maxJump = getMaxScrollAmount();
+
+        if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) {
+            nextFocused.getDrawingRect(mTempRect);
+            offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+            doScrollY(scrollDelta);
+            nextFocused.requestFocus(direction);
+        } else {
+            // no new focus
+            int scrollDelta = maxJump;
+
+            if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
+                scrollDelta = getScrollY();
+            } else if (direction == View.FOCUS_DOWN) {
+                if (getChildCount() > 0) {
+                    int daBottom = getChildAt(0).getBottom();
+                    int screenBottom = getScrollY() + getHeight() - getPaddingBottom();
+                    if (daBottom - screenBottom < maxJump) {
+                        scrollDelta = daBottom - screenBottom;
+                    }
+                }
+            }
+            if (scrollDelta == 0) {
+                return false;
+            }
+            doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+        }
+
+        if (currentFocused != null && currentFocused.isFocused()
+                && isOffScreen(currentFocused)) {
+            // previously focused item still has focus and is off screen, give
+            // it up (take it back to ourselves)
+            // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+            // sure to
+            // get it)
+            final int descendantFocusability = getDescendantFocusability();  // save
+            setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+            requestFocus();
+            setDescendantFocusability(descendantFocusability);  // restore
+        }
+        return true;
+    }
+
+    private boolean isOrthoMove(float moveX, float moveY) {
+        return mHorizontal && Math.abs(moveY) > Math.abs(moveX)
+                || !mHorizontal && Math.abs(moveX) > Math.abs(moveY);
+    }
+
+    /**
+     * @return whether the descendant of this scroll view is scrolled off
+     *  screen.
+     */
+    private boolean isOffScreen(View descendant) {
+        if (mHorizontal) {
+            return !isWithinDeltaOfScreen(descendant, getWidth(), 0);
+        } else {
+            return !isWithinDeltaOfScreen(descendant, 0, getHeight());
+        }
+    }
+
+    /**
+     * @return whether the descendant of this scroll view is within delta
+     *  pixels of being on the screen.
+     */
+    private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
+        descendant.getDrawingRect(mTempRect);
+        offsetDescendantRectToMyCoords(descendant, mTempRect);
+        if (mHorizontal) {
+            return (mTempRect.right + delta) >= getScrollX()
+            && (mTempRect.left - delta) <= (getScrollX() + height);
+        } else {
+            return (mTempRect.bottom + delta) >= getScrollY()
+            && (mTempRect.top - delta) <= (getScrollY() + height);
+        }
+    }
+
+    /**
+     * Smooth scroll by a Y delta
+     *
+     * @param delta the number of pixels to scroll by on the Y axis
+     */
+    private void doScrollY(int delta) {
+        if (delta != 0) {
+            if (mSmoothScrollingEnabled) {
+                if (mHorizontal) {
+                    smoothScrollBy(0, delta);
+                } else {
+                    smoothScrollBy(delta, 0);
+                }
+            } else {
+                if (mHorizontal) {
+                    scrollBy(0, delta);
+                } else {
+                    scrollBy(delta, 0);
+                }
+            }
+        }
+    }
+
+    /**
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param dx the number of pixels to scroll by on the X axis
+     * @param dy the number of pixels to scroll by on the Y axis
+     */
+    public final void smoothScrollBy(int dx, int dy) {
+        if (getChildCount() == 0) {
+            // Nothing to do.
+            return;
+        }
+        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+        if (duration > ANIMATED_SCROLL_GAP) {
+            if (mHorizontal) {
+                final int width = getWidth() - getPaddingRight() - getPaddingLeft();
+                final int right = getChildAt(0).getWidth();
+                final int maxX = Math.max(0, right - width);
+                final int scrollX = getScrollX();
+                dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
+                mScroller.startScroll(scrollX, getScrollY(), dx, 0);
+            } else {
+                final int height = getHeight() - getPaddingBottom() - getPaddingTop();
+                final int bottom = getChildAt(0).getHeight();
+                final int maxY = Math.max(0, bottom - height);
+                final int scrollY = getScrollY();
+                dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
+                mScroller.startScroll(getScrollX(), scrollY, 0, dy);
+            }
+            invalidate();
+        } else {
+            if (!mScroller.isFinished()) {
+                mScroller.abortAnimation();
+                if (mFlingStrictSpan != null) {
+                    mFlingStrictSpan.finish();
+                    mFlingStrictSpan = null;
+                }
+            }
+            scrollBy(dx, dy);
+        }
+        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+    }
+
+    /**
+     * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
+     *
+     * @param x the position where to scroll on the X axis
+     * @param y the position where to scroll on the Y axis
+     */
+    public final void smoothScrollTo(int x, int y) {
+        smoothScrollBy(x - getScrollX(), y - getScrollY());
+    }
+
+    /**
+     * <p>
+     * The scroll range of a scroll view is the overall height of all of its
+     * children.
+     * </p>
+     */
+    @Override
+    protected int computeVerticalScrollRange() {
+        if (mHorizontal) {
+            return super.computeVerticalScrollRange();
+        }
+        final int count = getChildCount();
+        final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop();
+        if (count == 0) {
+            return contentHeight;
+        }
+
+        int scrollRange = getChildAt(0).getBottom();
+        final int scrollY = getScrollY();
+        final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
+        if (scrollY < 0) {
+            scrollRange -= scrollY;
+        } else if (scrollY > overscrollBottom) {
+            scrollRange += scrollY - overscrollBottom;
+        }
+
+        return scrollRange;
+    }
+
+    /**
+     * <p>
+     * The scroll range of a scroll view is the overall height of all of its
+     * children.
+     * </p>
+     */
+    @Override
+    protected int computeHorizontalScrollRange() {
+        if (!mHorizontal) {
+            return super.computeHorizontalScrollRange();
+        }
+        final int count = getChildCount();
+        final int contentWidth = getWidth() - getPaddingRight() - getPaddingLeft();
+        if (count == 0) {
+            return contentWidth;
+        }
+
+        int scrollRange = getChildAt(0).getRight();
+        final int scrollX = getScrollX();
+        final int overscrollBottom = Math.max(0, scrollRange - contentWidth);
+        if (scrollX < 0) {
+            scrollRange -= scrollX;
+        } else if (scrollX > overscrollBottom) {
+            scrollRange += scrollX - overscrollBottom;
+        }
+
+        return scrollRange;
+    }
+
+    @Override
+    protected int computeVerticalScrollOffset() {
+        return Math.max(0, super.computeVerticalScrollOffset());
+    }
+
+    @Override
+    protected int computeHorizontalScrollOffset() {
+        return Math.max(0, super.computeHorizontalScrollOffset());
+    }
+
+    @Override
+    protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
+        ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+        int childWidthMeasureSpec;
+        int childHeightMeasureSpec;
+
+        if (mHorizontal) {
+            childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, getPaddingTop()
+                    + getPaddingBottom(), lp.height);
+
+            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        } else {
+            childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft()
+                    + getPaddingRight(), lp.width);
+
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        }
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+            int parentHeightMeasureSpec, int heightUsed) {
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        int childWidthMeasureSpec;
+        int childHeightMeasureSpec;
+        if (mHorizontal) {
+            childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+                    getPaddingTop() + getPaddingBottom() + lp.topMargin + lp.bottomMargin
+                            + heightUsed, lp.height);
+            childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED);
+        } else {
+            childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+                    getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin
+                            + widthUsed, lp.width);
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED);
+        }
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            // This is called at drawing time by ViewGroup.  We don't want to
+            // re-show the scrollbars at this point, which scrollTo will do,
+            // so we replicate most of scrollTo here.
+            //
+            //         It's a little odd to call onScrollChanged from inside the drawing.
+            //
+            //         It is, except when you remember that computeScroll() is used to
+            //         animate scrolling. So unless we want to defer the onScrollChanged()
+            //         until the end of the animated scrolling, we don't really have a
+            //         choice here.
+            //
+            //         I agree.  The alternative, which I think would be worse, is to post
+            //         something and tell the subclasses later.  This is bad because there
+            //         will be a window where getScrollX()/Y is different from what the app
+            //         thinks it is.
+            //
+            int oldX = getScrollX();
+            int oldY = getScrollY();
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+
+            if (oldX != x || oldY != y) {
+                if (mHorizontal) {
+                    overScrollBy(x - oldX, y - oldY, oldX, oldY, getScrollRange(), 0,
+                            mOverflingDistance, 0, false);
+                } else {
+                    overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, getScrollRange(),
+                            0, mOverflingDistance, false);
+                }
+                onScrollChanged(getScrollX(), getScrollY(), oldX, oldY);
+            }
+            awakenScrollBars();
+
+            // Keep on drawing until the animation has finished.
+            postInvalidate();
+        } else {
+            if (mFlingStrictSpan != null) {
+                mFlingStrictSpan.finish();
+                mFlingStrictSpan = null;
+            }
+        }
+    }
+
+    /**
+     * Scrolls the view to the given child.
+     *
+     * @param child the View to scroll to
+     */
+    private void scrollToChild(View child) {
+        child.getDrawingRect(mTempRect);
+
+        /* Offset from child's local coordinates to ScrollView coordinates */
+        offsetDescendantRectToMyCoords(child, mTempRect);
+        scrollToChildRect(mTempRect, true);
+    }
+
+    /**
+     * If rect is off screen, scroll just enough to get it (or at least the
+     * first screen size chunk of it) on screen.
+     *
+     * @param rect      The rectangle.
+     * @param immediate True to scroll immediately without animation
+     * @return true if scrolling was performed
+     */
+    private boolean scrollToChildRect(Rect rect, boolean immediate) {
+        final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
+        final boolean scroll = delta != 0;
+        if (scroll) {
+            if (immediate) {
+                if (mHorizontal) {
+                    scrollBy(delta, 0);
+                } else {
+                    scrollBy(0, delta);
+                }
+            } else {
+                if (mHorizontal) {
+                    smoothScrollBy(delta, 0);
+                } else {
+                    smoothScrollBy(0, delta);
+                }
+            }
+        }
+        return scroll;
+    }
+
+    /**
+     * Compute the amount to scroll in the Y direction in order to get
+     * a rectangle completely on the screen (or, if taller than the screen,
+     * at least the first screen size chunk of it).
+     *
+     * @param rect The rect.
+     * @return The scroll delta.
+     */
+    protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
+        if (mHorizontal) {
+            return computeScrollDeltaToGetChildRectOnScreenHorizontal(rect);
+        } else {
+            return computeScrollDeltaToGetChildRectOnScreenVertical(rect);
+        }
+    }
+
+    private int computeScrollDeltaToGetChildRectOnScreenVertical(Rect rect) {
+        if (getChildCount() == 0) return 0;
+
+        int height = getHeight();
+        int screenTop = getScrollY();
+        int screenBottom = screenTop + height;
+
+        int fadingEdge = getVerticalFadingEdgeLength();
+
+        // leave room for top fading edge as long as rect isn't at very top
+        if (rect.top > 0) {
+            screenTop += fadingEdge;
+        }
+
+        // leave room for bottom fading edge as long as rect isn't at very bottom
+        if (rect.bottom < getChildAt(0).getHeight()) {
+            screenBottom -= fadingEdge;
+        }
+
+        int scrollYDelta = 0;
+
+        if (rect.bottom > screenBottom && rect.top > screenTop) {
+            // need to move down to get it in view: move down just enough so
+            // that the entire rectangle is in view (or at least the first
+            // screen size chunk).
+
+            if (rect.height() > height) {
+                // just enough to get screen size chunk on
+                scrollYDelta += (rect.top - screenTop);
+            } else {
+                // get entire rect at bottom of screen
+                scrollYDelta += (rect.bottom - screenBottom);
+            }
+
+            // make sure we aren't scrolling beyond the end of our content
+            int bottom = getChildAt(0).getBottom();
+            int distanceToBottom = bottom - screenBottom;
+            scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+
+        } else if (rect.top < screenTop && rect.bottom < screenBottom) {
+            // need to move up to get it in view: move up just enough so that
+            // entire rectangle is in view (or at least the first screen
+            // size chunk of it).
+
+            if (rect.height() > height) {
+                // screen size chunk
+                scrollYDelta -= (screenBottom - rect.bottom);
+            } else {
+                // entire rect at top
+                scrollYDelta -= (screenTop - rect.top);
+            }
+
+            // make sure we aren't scrolling any further than the top our content
+            scrollYDelta = Math.max(scrollYDelta, -getScrollY());
+        }
+        return scrollYDelta;
+    }
+
+    private int computeScrollDeltaToGetChildRectOnScreenHorizontal(Rect rect) {
+        if (getChildCount() == 0) return 0;
+
+        int width = getWidth();
+        int screenLeft = getScrollX();
+        int screenRight = screenLeft + width;
+
+        int fadingEdge = getHorizontalFadingEdgeLength();
+
+        // leave room for left fading edge as long as rect isn't at very left
+        if (rect.left > 0) {
+            screenLeft += fadingEdge;
+        }
+
+        // leave room for right fading edge as long as rect isn't at very right
+        if (rect.right < getChildAt(0).getWidth()) {
+            screenRight -= fadingEdge;
+        }
+
+        int scrollXDelta = 0;
+
+        if (rect.right > screenRight && rect.left > screenLeft) {
+            // need to move right to get it in view: move right just enough so
+            // that the entire rectangle is in view (or at least the first
+            // screen size chunk).
+
+            if (rect.width() > width) {
+                // just enough to get screen size chunk on
+                scrollXDelta += (rect.left - screenLeft);
+            } else {
+                // get entire rect at right of screen
+                scrollXDelta += (rect.right - screenRight);
+            }
+
+            // make sure we aren't scrolling beyond the end of our content
+            int right = getChildAt(0).getRight();
+            int distanceToRight = right - screenRight;
+            scrollXDelta = Math.min(scrollXDelta, distanceToRight);
+
+        } else if (rect.left < screenLeft && rect.right < screenRight) {
+            // need to move right to get it in view: move right just enough so that
+            // entire rectangle is in view (or at least the first screen
+            // size chunk of it).
+
+            if (rect.width() > width) {
+                // screen size chunk
+                scrollXDelta -= (screenRight - rect.right);
+            } else {
+                // entire rect at left
+                scrollXDelta -= (screenLeft - rect.left);
+            }
+
+            // make sure we aren't scrolling any further than the left our content
+            scrollXDelta = Math.max(scrollXDelta, -getScrollX());
+        }
+        return scrollXDelta;
+    }
+
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (!mIsLayoutDirty) {
+            scrollToChild(focused);
+        } else {
+            // The child may not be laid out yet, we can't compute the scroll yet
+            mChildToScrollTo = focused;
+        }
+        super.requestChildFocus(child, focused);
+    }
+
+
+    /**
+     * When looking for focus in children of a scroll view, need to be a little
+     * more careful not to give focus to something that is scrolled off screen.
+     *
+     * This is more expensive than the default {@link android.view.ViewGroup}
+     * implementation, otherwise this behavior might have been made the default.
+     */
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction,
+            Rect previouslyFocusedRect) {
+
+        // convert from forward / backward notation to up / down / left / right
+        // (ugh).
+        if (mHorizontal) {
+            if (direction == View.FOCUS_FORWARD) {
+                direction = View.FOCUS_RIGHT;
+            } else if (direction == View.FOCUS_BACKWARD) {
+                direction = View.FOCUS_LEFT;
+            }
+        } else {
+            if (direction == View.FOCUS_FORWARD) {
+                direction = View.FOCUS_DOWN;
+            } else if (direction == View.FOCUS_BACKWARD) {
+                direction = View.FOCUS_UP;
+            }
+        }
+
+        final View nextFocus = previouslyFocusedRect == null ?
+                FocusFinder.getInstance().findNextFocus(this, null, direction) :
+                FocusFinder.getInstance().findNextFocusFromRect(this,
+                        previouslyFocusedRect, direction);
+
+        if (nextFocus == null) {
+            return false;
+        }
+
+        if (isOffScreen(nextFocus)) {
+            return false;
+        }
+
+        return nextFocus.requestFocus(direction, previouslyFocusedRect);
+    }
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+            boolean immediate) {
+        // offset into coordinate space of this scroll view
+        rectangle.offset(child.getLeft() - child.getScrollX(),
+                child.getTop() - child.getScrollY());
+
+        return scrollToChildRect(rectangle, immediate);
+    }
+
+    @Override
+    public void requestLayout() {
+        mIsLayoutDirty = true;
+        super.requestLayout();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mScrollStrictSpan != null) {
+            mScrollStrictSpan.finish();
+            mScrollStrictSpan = null;
+        }
+        if (mFlingStrictSpan != null) {
+            mFlingStrictSpan.finish();
+            mFlingStrictSpan = null;
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mIsLayoutDirty = false;
+        // Give a child focus if it needs it
+        if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+            scrollToChild(mChildToScrollTo);
+        }
+        mChildToScrollTo = null;
+
+        // Calling this with the present values causes it to re-clam them
+        scrollTo(getScrollX(), getScrollY());
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        View currentFocused = findFocus();
+        if (null == currentFocused || this == currentFocused)
+            return;
+
+        // If the currently-focused view was visible on the screen when the
+        // screen was at the old height, then scroll the screen to make that
+        // view visible with the new screen height.
+        if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
+            currentFocused.getDrawingRect(mTempRect);
+            offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+            doScrollY(scrollDelta);
+        }
+    }
+
+    /**
+     * Return true if child is an descendant of parent, (or equal to the parent).
+     */
+    private boolean isViewDescendantOf(View child, View parent) {
+        if (child == parent) {
+            return true;
+        }
+
+        final ViewParent theParent = child.getParent();
+        return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+    }
+
+    /**
+     * Fling the scroll view
+     *
+     * @param velocityY The initial velocity in the Y direction. Positive
+     *                  numbers mean that the finger/cursor is moving down the screen,
+     *                  which means we want to scroll towards the top.
+     */
+    public void fling(int velocityY) {
+        if (getChildCount() > 0) {
+            if (mHorizontal) {
+                int width = getWidth() - getPaddingRight() - getPaddingLeft();
+                int right = getChildAt(0).getWidth();
+
+                mScroller.fling(getScrollX(), getScrollY(), velocityY, 0,
+                        0, Math.max(0, right - width), 0, 0, width/2, 0);
+            } else {
+                int height = getHeight() - getPaddingBottom() - getPaddingTop();
+                int bottom = getChildAt(0).getHeight();
+
+                mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
+                        Math.max(0, bottom - height), 0, height/2);
+            }
+            if (mFlingStrictSpan == null) {
+                //mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
+                mFlingStrictSpan = NO_OP_SPAN;
+            }
+
+            invalidate();
+        }
+    }
+
+    private void endDrag() {
+        mIsBeingDragged = false;
+        mIsOrthoDragged = false;
+        mDownView = null;
+        recycleVelocityTracker();
+        if (mScrollStrictSpan != null) {
+            mScrollStrictSpan.finish();
+            mScrollStrictSpan = null;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This version also clamps the scrolling to the bounds of our child.
+     */
+    @Override
+    public void scrollTo(int x, int y) {
+        // we rely on the fact the View.scrollBy calls scrollTo.
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth());
+            y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight());
+            if (x != getScrollX() || y != getScrollY()) {
+                super.scrollTo(x, y);
+            }
+        }
+    }
+
+    private int clamp(int n, int my, int child) {
+        if (my >= child || n < 0) {
+            /* my >= child is this case:
+             *                    |--------------- me ---------------|
+             *     |------ child ------|
+             * or
+             *     |--------------- me ---------------|
+             *            |------ child ------|
+             * or
+             *     |--------------- me ---------------|
+             *                                  |------ child ------|
+             *
+             * n < 0 is this case:
+             *     |------ me ------|
+             *                    |-------- child --------|
+             *     |-- getScrollX() --|
+             */
+            return 0;
+        }
+        if ((my+n) > child) {
+            /* this case:
+             *                    |------ me ------|
+             *     |------ child ------|
+             *     |-- getScrollX() --|
+             */
+            return child-my;
+        }
+        return n;
+    }
+
+}
diff --git a/src/com/android/browser/view/SnapshotGridView.java b/src/com/android/browser/view/SnapshotGridView.java
new file mode 100644
index 0000000..ab12060
--- /dev/null
+++ b/src/com/android/browser/view/SnapshotGridView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2011 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.view;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.GridView;
+
+public class SnapshotGridView extends GridView {
+
+    private static final int MAX_COLUMNS = 5;
+
+    private int mColWidth;
+
+    public SnapshotGridView(Context context) {
+        super(context);
+    }
+
+    public SnapshotGridView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public SnapshotGridView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        if (widthSize > 0 && mColWidth > 0) {
+            int numCols = widthSize / mColWidth;
+            widthSize = Math.min(
+                    Math.min(numCols, MAX_COLUMNS) * mColWidth,
+                    widthSize);
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+        }
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    @Override
+    public void setColumnWidth(int columnWidth) {
+        mColWidth = columnWidth;
+        super.setColumnWidth(columnWidth);
+    }
+}
diff --git a/src/com/android/browser/view/StopProgressView.java b/src/com/android/browser/view/StopProgressView.java
new file mode 100644
index 0000000..3df099d
--- /dev/null
+++ b/src/com/android/browser/view/StopProgressView.java
@@ -0,0 +1,98 @@
+
+package com.android.browser.view;
+
+import com.android.browser.R;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.widget.ProgressBar;
+
+
+public class StopProgressView extends ProgressBar {
+
+    Drawable mOverlayDrawable;
+    Drawable mProgressDrawable;
+    int mWidth;
+    int mHeight;
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     * @param styleRes
+     */
+    public StopProgressView(Context context, AttributeSet attrs, int defStyle, int styleRes) {
+        super(context, attrs, defStyle);
+        init(attrs);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     * @param defStyle
+     */
+    public StopProgressView(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+        init(attrs);
+    }
+
+    /**
+     * @param context
+     * @param attrs
+     */
+    public StopProgressView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        init(attrs);
+    }
+
+    /**
+     * @param context
+     */
+    public StopProgressView(Context context) {
+        super(context);
+        init(null);
+    }
+
+    private void init(AttributeSet attrs) {
+        mProgressDrawable = getIndeterminateDrawable();
+        setImageDrawable(getContext().getResources()
+                .getDrawable(R.drawable.ic_stop_holo_dark));
+    }
+
+    public void hideProgress() {
+        setIndeterminateDrawable(null);
+    }
+
+    public void showProgress() {
+        setIndeterminateDrawable(mProgressDrawable);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        mWidth = (right - left) * 2 / 3;
+        mHeight = (bottom - top) * 2 / 3;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        if (mOverlayDrawable != null) {
+            int l = (getWidth() - mWidth) / 2;
+            int t = (getHeight() - mHeight) / 2;
+            mOverlayDrawable.setBounds(l, t, l + mWidth, t + mHeight);
+            mOverlayDrawable.draw(canvas);
+        }
+    }
+
+    public Drawable getDrawable() {
+        return mOverlayDrawable;
+    }
+
+    public void setImageDrawable(Drawable d) {
+        mOverlayDrawable = d;
+    }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java b/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java
new file mode 100644
index 0000000..f3d2675
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkThumbnailWidgetProvider.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2010 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.widget;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.widget.RemoteViews;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.R;
+
+/**
+ * Widget that shows a preview of the user's bookmarks.
+ */
+public class BookmarkThumbnailWidgetProvider extends AppWidgetProvider {
+    public static final String ACTION_BOOKMARK_APPWIDGET_UPDATE =
+        "com.android.browser.BOOKMARK_APPWIDGET_UPDATE";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        // Handle bookmark-specific updates ourselves because they might be
+        // coming in without extras, which AppWidgetProvider then blocks.
+        final String action = intent.getAction();
+        if (ACTION_BOOKMARK_APPWIDGET_UPDATE.equals(action)) {
+            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+            performUpdate(context, appWidgetManager,
+                    appWidgetManager.getAppWidgetIds(getComponentName(context)));
+        } else {
+            super.onReceive(context, intent);
+        }
+    }
+
+    @Override
+    public void onUpdate(Context context, AppWidgetManager mngr, int[] ids) {
+        performUpdate(context, mngr, ids);
+    }
+
+    @Override
+    public void onDeleted(Context context, int[] appWidgetIds) {
+        super.onDeleted(context, appWidgetIds);
+        for (int widgetId : appWidgetIds) {
+            BookmarkThumbnailWidgetService.deleteWidgetState(context, widgetId);
+        }
+        removeOrphanedFiles(context);
+    }
+
+    @Override
+    public void onDisabled(Context context) {
+        super.onDisabled(context);
+        removeOrphanedFiles(context);
+    }
+
+    /**
+     *  Checks for any state files that may have not received onDeleted
+     */
+    void removeOrphanedFiles(Context context) {
+        AppWidgetManager wm = AppWidgetManager.getInstance(context);
+        int[] ids = wm.getAppWidgetIds(getComponentName(context));
+        BookmarkThumbnailWidgetService.removeOrphanedStates(context, ids);
+    }
+
+    private void performUpdate(Context context,
+            AppWidgetManager appWidgetManager, int[] appWidgetIds) {
+        PendingIntent launchBrowser = PendingIntent.getActivity(context, 0,
+                new Intent(BrowserActivity.ACTION_SHOW_BROWSER, null, context,
+                    BrowserActivity.class),
+                PendingIntent.FLAG_UPDATE_CURRENT);
+        for (int appWidgetId : appWidgetIds) {
+            Intent updateIntent = new Intent(context, BookmarkThumbnailWidgetService.class);
+            updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
+            updateIntent.setData(Uri.parse(updateIntent.toUri(Intent.URI_INTENT_SCHEME)));
+            RemoteViews views = new RemoteViews(context.getPackageName(),
+                    R.layout.bookmarkthumbnailwidget);
+            views.setOnClickPendingIntent(R.id.app_shortcut, launchBrowser);
+            views.setRemoteAdapter(R.id.bookmarks_list, updateIntent);
+            appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.bookmarks_list);
+            Intent ic = new Intent(context, BookmarkWidgetProxy.class);
+            views.setPendingIntentTemplate(R.id.bookmarks_list,
+                    PendingIntent.getBroadcast(context, 0, ic,
+                    PendingIntent.FLAG_UPDATE_CURRENT));
+            appWidgetManager.updateAppWidget(appWidgetId, views);
+        }
+    }
+
+    /**
+     * Build {@link ComponentName} describing this specific
+     * {@link AppWidgetProvider}
+     */
+    static ComponentName getComponentName(Context context) {
+        return new ComponentName(context, BookmarkThumbnailWidgetProvider.class);
+    }
+
+    public static void refreshWidgets(Context context) {
+        context.sendBroadcast(new Intent(
+                BookmarkThumbnailWidgetProvider.ACTION_BOOKMARK_APPWIDGET_UPDATE,
+                null, context, BookmarkThumbnailWidgetProvider.class));
+    }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java b/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java
new file mode 100644
index 0000000..6f5e3b2
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkThumbnailWidgetService.java
@@ -0,0 +1,378 @@
+/*
+ * Copyright (C) 2010 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.widget;
+
+import android.appwidget.AppWidgetManager;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.MergeCursor;
+import android.graphics.Bitmap;
+import android.graphics.Bitmap.Config;
+import android.graphics.BitmapFactory;
+import android.graphics.BitmapFactory.Options;
+import android.net.Uri;
+import android.os.Binder;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.RemoteViews;
+import android.widget.RemoteViewsService;
+
+import com.android.browser.BrowserActivity;
+import com.android.browser.R;
+import com.android.browser.platformsupport.BrowserContract;
+import com.android.browser.platformsupport.BrowserContract.Bookmarks;
+import com.android.browser.provider.BrowserProvider2;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.util.HashSet;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class BookmarkThumbnailWidgetService extends RemoteViewsService {
+
+    static final String TAG = "BookmarkThumbnailWidgetService";
+    static final String ACTION_CHANGE_FOLDER = "com.android.browser.widget.CHANGE_FOLDER";
+    static final String STATE_CURRENT_FOLDER = "current_folder";
+    static final String STATE_ROOT_FOLDER = "root_folder";
+
+    private static final String[] PROJECTION = new String[] {
+            BrowserContract.Bookmarks._ID,
+            BrowserContract.Bookmarks.TITLE,
+            BrowserContract.Bookmarks.URL,
+            BrowserContract.Bookmarks.FAVICON,
+            BrowserContract.Bookmarks.IS_FOLDER,
+            BrowserContract.Bookmarks.POSITION, /* needed for order by */
+            BrowserContract.Bookmarks.THUMBNAIL,
+            BrowserContract.Bookmarks.PARENT};
+    private static final int BOOKMARK_INDEX_ID = 0;
+    private static final int BOOKMARK_INDEX_TITLE = 1;
+    private static final int BOOKMARK_INDEX_URL = 2;
+    private static final int BOOKMARK_INDEX_FAVICON = 3;
+    private static final int BOOKMARK_INDEX_IS_FOLDER = 4;
+    private static final int BOOKMARK_INDEX_THUMBNAIL = 6;
+    private static final int BOOKMARK_INDEX_PARENT_ID = 7;
+
+    @Override
+    public RemoteViewsFactory onGetViewFactory(Intent intent) {
+        int widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+        if (widgetId < 0) {
+            Log.w(TAG, "Missing EXTRA_APPWIDGET_ID!");
+            return null;
+        }
+        return new BookmarkFactory(getApplicationContext(), widgetId);
+    }
+
+    static SharedPreferences getWidgetState(Context context, int widgetId) {
+        return context.getSharedPreferences(
+                String.format("widgetState-%d", widgetId),
+                Context.MODE_PRIVATE);
+    }
+
+    static private File mPreferencesDir;
+    static File getPreferencesDir(Context context) {
+            if (mPreferencesDir == null) {
+                mPreferencesDir = new File(context.getApplicationInfo().dataDir, "shared_prefs");
+            }
+            return mPreferencesDir;
+    }
+    static File makeFilename(File base, String name) {
+        if (name.indexOf(File.separatorChar) < 0) {
+            return new File(base, name);
+        }
+        throw new IllegalArgumentException(
+                "File " + name + " contains a path separator");
+    }
+    static File getSharedPrefsFile(Context context, String name) {
+        return makeFilename(getPreferencesDir(context), name + ".xml");
+    }
+
+    static void deleteWidgetState(Context context, int widgetId) {
+        File file = getSharedPrefsFile(context,
+                String.format("widgetState-%d", widgetId));
+        if (file.exists()) {
+            if (!file.delete()) {
+                file.deleteOnExit();
+            }
+        }
+    }
+
+    static void changeFolder(Context context, Intent intent) {
+        int wid = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1);
+        long fid = intent.getLongExtra(Bookmarks._ID, -1);
+        if (wid >= 0 && fid >= 0) {
+            SharedPreferences prefs = getWidgetState(context, wid);
+            prefs.edit().putLong(STATE_CURRENT_FOLDER, fid).commit();
+            AppWidgetManager.getInstance(context)
+                    .notifyAppWidgetViewDataChanged(wid, R.id.bookmarks_list);
+        }
+    }
+
+    static void setupWidgetState(Context context, int widgetId, long rootFolder) {
+        SharedPreferences pref = getWidgetState(context, widgetId);
+        pref.edit()
+            .putLong(STATE_CURRENT_FOLDER, rootFolder)
+            .putLong(STATE_ROOT_FOLDER, rootFolder)
+            .apply();
+    }
+
+    /**
+     *  Checks for any state files that may have not received onDeleted
+     */
+    static void removeOrphanedStates(Context context, int[] widgetIds) {
+        File prefsDirectory = getSharedPrefsFile(context, "null").getParentFile();
+        File[] widgetStates = prefsDirectory.listFiles(new StateFilter(widgetIds));
+        if (widgetStates != null) {
+            for (File f : widgetStates) {
+                Log.w(TAG, "Found orphaned state: " + f.getName());
+                if (!f.delete()) {
+                    f.deleteOnExit();
+                }
+            }
+        }
+    }
+
+    static class StateFilter implements FilenameFilter {
+
+        static final Pattern sStatePattern = Pattern.compile("widgetState-(\\d+)\\.xml");
+        HashSet<Integer> mWidgetIds;
+
+        StateFilter(int[] ids) {
+            mWidgetIds = new HashSet<Integer>();
+            for (int id : ids) {
+                mWidgetIds.add(id);
+            }
+        }
+
+        @Override
+        public boolean accept(File dir, String filename) {
+            Matcher m = sStatePattern.matcher(filename);
+            if (m.matches()) {
+                int id = Integer.parseInt(m.group(1));
+                if (!mWidgetIds.contains(id)) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+    }
+
+    static class BookmarkFactory implements RemoteViewsService.RemoteViewsFactory {
+        private Cursor mBookmarks;
+        private Context mContext;
+        private int mWidgetId;
+        private long mCurrentFolder = -1;
+        private long mRootFolder = -1;
+        private SharedPreferences mPreferences = null;
+
+        public BookmarkFactory(Context context, int widgetId) {
+            mContext = context.getApplicationContext();
+            mWidgetId = widgetId;
+        }
+
+        void syncState() {
+            if (mPreferences == null) {
+                mPreferences = getWidgetState(mContext, mWidgetId);
+            }
+            long currentFolder = mPreferences.getLong(STATE_CURRENT_FOLDER, -1);
+            mRootFolder = mPreferences.getLong(STATE_ROOT_FOLDER, -1);
+            if (currentFolder != mCurrentFolder) {
+                resetBookmarks();
+                mCurrentFolder = currentFolder;
+            }
+        }
+
+        void saveState() {
+            if (mPreferences == null) {
+                mPreferences = getWidgetState(mContext, mWidgetId);
+            }
+            mPreferences.edit()
+                .putLong(STATE_CURRENT_FOLDER, mCurrentFolder)
+                .putLong(STATE_ROOT_FOLDER, mRootFolder)
+                .commit();
+        }
+
+        @Override
+        public int getCount() {
+            if (mBookmarks == null)
+                return 0;
+            return mBookmarks.getCount();
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public RemoteViews getLoadingView() {
+            return new RemoteViews(
+                    mContext.getPackageName(), R.layout.bookmarkthumbnailwidget_item);
+        }
+
+        @Override
+        public RemoteViews getViewAt(int position) {
+            if (!mBookmarks.moveToPosition(position)) {
+                return null;
+            }
+
+            long id = mBookmarks.getLong(BOOKMARK_INDEX_ID);
+            String title = mBookmarks.getString(BOOKMARK_INDEX_TITLE);
+            String url = mBookmarks.getString(BOOKMARK_INDEX_URL);
+            boolean isFolder = mBookmarks.getInt(BOOKMARK_INDEX_IS_FOLDER) != 0;
+
+            RemoteViews views;
+            // Two layouts are needed because of b/5387153
+            if (isFolder) {
+                views = new RemoteViews(mContext.getPackageName(),
+                        R.layout.bookmarkthumbnailwidget_item_folder);
+            } else {
+                views = new RemoteViews(mContext.getPackageName(),
+                        R.layout.bookmarkthumbnailwidget_item);
+            }
+            // Set the title of the bookmark. Use the url as a backup.
+            String displayTitle = title;
+            if (TextUtils.isEmpty(displayTitle)) {
+                // The browser always requires a title for bookmarks, but jic...
+                displayTitle = url;
+            }
+            views.setTextViewText(R.id.label, displayTitle);
+            if (isFolder) {
+                if (id == mCurrentFolder) {
+                    id = mBookmarks.getLong(BOOKMARK_INDEX_PARENT_ID);
+                    views.setImageViewResource(R.id.thumb,
+                        R.drawable.thumb_bookmark_widget_folder_back_holo);
+                } else {
+                    views.setImageViewResource(R.id.thumb,
+                        R.drawable.thumb_bookmark_widget_folder_holo);
+                }
+                views.setImageViewResource(R.id.favicon,
+                    R.drawable.ic_bookmark_widget_bookmark_holo_dark);
+                // SWE_TODO : Fix Me
+                //views.setDrawableParameters(R.id.thumb, true, 0, -1, null, -1);
+            } else {
+                // RemoteViews require a valid bitmap config
+                Options options = new Options();
+                options.inPreferredConfig = Config.ARGB_8888;
+                Bitmap thumbnail = null, favicon = null;
+                byte[] blob = mBookmarks.getBlob(BOOKMARK_INDEX_THUMBNAIL);
+                // SWE_TODO : Fix Me
+                //views.setDrawableParameters(R.id.thumb, true, 255, -1, null, -1);
+                if (blob != null && blob.length > 0) {
+                    thumbnail = BitmapFactory.decodeByteArray(
+                            blob, 0, blob.length, options);
+                    views.setImageViewBitmap(R.id.thumb, thumbnail);
+                } else {
+                    views.setImageViewResource(R.id.thumb,
+                            R.drawable.browser_thumbnail);
+                }
+                blob = mBookmarks.getBlob(BOOKMARK_INDEX_FAVICON);
+                if (blob != null && blob.length > 0) {
+                    favicon = BitmapFactory.decodeByteArray(
+                            blob, 0, blob.length, options);
+                    views.setImageViewBitmap(R.id.favicon, favicon);
+                } else {
+                    views.setImageViewResource(R.id.favicon,
+                            R.drawable.app_web_browser_sm);
+                }
+            }
+            Intent fillin;
+            if (isFolder) {
+                fillin = new Intent(ACTION_CHANGE_FOLDER)
+                        .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mWidgetId)
+                        .putExtra(Bookmarks._ID, id);
+            } else {
+                if (!TextUtils.isEmpty(url)) {
+                    fillin = new Intent(Intent.ACTION_VIEW)
+                            .addCategory(Intent.CATEGORY_BROWSABLE)
+                            .setData(Uri.parse(url));
+                } else {
+                    fillin = new Intent(BrowserActivity.ACTION_SHOW_BROWSER);
+                }
+            }
+            views.setOnClickFillInIntent(R.id.list_item, fillin);
+            return views;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 2;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return false;
+        }
+
+        @Override
+        public void onCreate() {
+        }
+
+        @Override
+        public void onDestroy() {
+            if (mBookmarks != null) {
+                mBookmarks.close();
+                mBookmarks = null;
+            }
+            deleteWidgetState(mContext, mWidgetId);
+        }
+
+        @Override
+        public void onDataSetChanged() {
+            long token = Binder.clearCallingIdentity();
+            syncState();
+            if (mRootFolder < 0 || mCurrentFolder < 0) {
+                // This shouldn't happen, but JIC default to the local account
+                mRootFolder = BrowserProvider2.FIXED_ID_ROOT;
+                mCurrentFolder = mRootFolder;
+                saveState();
+            }
+            loadBookmarks();
+            Binder.restoreCallingIdentity(token);
+        }
+
+        private void resetBookmarks() {
+            if (mBookmarks != null) {
+                mBookmarks.close();
+                mBookmarks = null;
+            }
+        }
+
+        void loadBookmarks() {
+            resetBookmarks();
+
+            Uri uri = ContentUris.withAppendedId(
+                    BrowserContract.Bookmarks.CONTENT_URI_DEFAULT_FOLDER,
+                    mCurrentFolder);
+            mBookmarks = mContext.getContentResolver().query(uri, PROJECTION,
+                    null, null, null);
+            if (mCurrentFolder != mRootFolder) {
+                uri = ContentUris.withAppendedId(
+                        BrowserContract.Bookmarks.CONTENT_URI,
+                        mCurrentFolder);
+                Cursor c = mContext.getContentResolver().query(uri, PROJECTION,
+                        null, null, null);
+                mBookmarks = new MergeCursor(new Cursor[] { c, mBookmarks });
+            }
+        }
+    }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkWidgetConfigure.java b/src/com/android/browser/widget/BookmarkWidgetConfigure.java
new file mode 100644
index 0000000..2dee989
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkWidgetConfigure.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2011 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.widget;
+
+import android.app.ListActivity;
+import android.app.LoaderManager.LoaderCallbacks;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Intent;
+import android.content.Loader;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.browser.R;
+import com.android.browser.AddBookmarkPage.BookmarkAccount;
+import com.android.browser.platformsupport.BrowserContract.Accounts;
+import com.android.browser.provider.BrowserProvider2;
+
+public class BookmarkWidgetConfigure extends ListActivity
+        implements OnClickListener, LoaderCallbacks<Cursor> {
+
+    static final int LOADER_ACCOUNTS = 1;
+
+    private ArrayAdapter<BookmarkAccount> mAccountAdapter;
+    private int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setResult(RESULT_CANCELED);
+        setVisible(false);
+        setContentView(R.layout.widget_account_selection);
+        findViewById(R.id.cancel).setOnClickListener(this);
+        mAccountAdapter = new ArrayAdapter<BookmarkAccount>(this,
+                android.R.layout.simple_list_item_1);
+        setListAdapter(mAccountAdapter);
+        Intent intent = getIntent();
+        Bundle extras = intent.getExtras();
+        if (extras != null) {
+            mAppWidgetId = extras.getInt(
+                    AppWidgetManager.EXTRA_APPWIDGET_ID,
+                    AppWidgetManager.INVALID_APPWIDGET_ID);
+        }
+        if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
+            finish();
+        } else {
+            getLoaderManager().initLoader(LOADER_ACCOUNTS, null, this);
+        }
+    }
+
+    @Override
+    public void onClick(View v) {
+        finish();
+    }
+
+    @Override
+    protected void onListItemClick(ListView l, View v, int position, long id) {
+        BookmarkAccount account = mAccountAdapter.getItem(position);
+        pickAccount(account.rootFolderId);
+    }
+
+    @Override
+    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
+        return new AccountsLoader(this);
+    }
+
+    void pickAccount(long rootId) {
+        BookmarkThumbnailWidgetService.setupWidgetState(this, mAppWidgetId, rootId);
+        Intent result = new Intent();
+        result.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
+        setResult(RESULT_OK, result);
+        finish();
+    }
+
+    @Override
+    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
+        if (cursor == null || cursor.getCount() < 1) {
+            // We always have the local account, so fall back to that
+            pickAccount(BrowserProvider2.FIXED_ID_ROOT);
+        } else if (cursor.getCount() == 1) {
+            cursor.moveToFirst();
+            pickAccount(cursor.getLong(AccountsLoader.COLUMN_INDEX_ROOT_ID));
+        } else {
+            mAccountAdapter.clear();
+            while (cursor.moveToNext()) {
+                mAccountAdapter.add(new BookmarkAccount(this, cursor));
+            }
+            setVisible(true);
+        }
+        getLoaderManager().destroyLoader(LOADER_ACCOUNTS);
+    }
+
+    @Override
+    public void onLoaderReset(Loader<Cursor> loader) {
+        // Don't care
+    }
+
+    static class AccountsLoader extends CursorLoader {
+
+        static final String[] PROJECTION = new String[] {
+            Accounts.ACCOUNT_NAME,
+            Accounts.ACCOUNT_TYPE,
+            Accounts.ROOT_ID,
+        };
+
+        static final int COLUMN_INDEX_ACCOUNT_NAME = 0;
+        static final int COLUMN_INDEX_ACCOUNT_TYPE = 1;
+        static final int COLUMN_INDEX_ROOT_ID = 2;
+
+        public AccountsLoader(Context context) {
+            super(context, Accounts.CONTENT_URI
+                    .buildUpon()
+                    .appendQueryParameter(BrowserProvider2.PARAM_ALLOW_EMPTY_ACCOUNTS, "false")
+                    .build(), PROJECTION, null, null, null);
+        }
+
+    }
+
+}
diff --git a/src/com/android/browser/widget/BookmarkWidgetProxy.java b/src/com/android/browser/widget/BookmarkWidgetProxy.java
new file mode 100644
index 0000000..8ab57fc
--- /dev/null
+++ b/src/com/android/browser/widget/BookmarkWidgetProxy.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2011 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.widget;
+
+import com.android.browser.BrowserActivity;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class BookmarkWidgetProxy extends BroadcastReceiver {
+
+    private static final String TAG = "BookmarkWidgetProxy";
+
+    @Override
+    public void onReceive(Context context, Intent intent) {
+        if (BookmarkThumbnailWidgetService.ACTION_CHANGE_FOLDER.equals(intent.getAction())) {
+            BookmarkThumbnailWidgetService.changeFolder(context, intent);
+        } else if (BrowserActivity.ACTION_SHOW_BROWSER.equals(intent.getAction())) {
+            startActivity(context,
+                    new Intent(BrowserActivity.ACTION_SHOW_BROWSER,
+                    null, context, BrowserActivity.class));
+        } else {
+            Intent view = new Intent(intent);
+            view.setComponent(null);
+            startActivity(context, view);
+        }
+    }
+
+    void startActivity(Context context, Intent intent) {
+        try {
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            context.startActivity(intent);
+        } catch (Exception e) {
+            Log.w(TAG, "Failed to start intent activity", e);
+        }
+    }
+}