Merge change 8853

* changes:
  Make the favicons look better on the bookmark page.
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 145889e..f3707d4 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -19,6 +19,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.browser">
 
     <uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH" />
+    <uses-permission android:name="android.permission.USE_CREDENTIALS" />
     <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
     <uses-permission android:name="android.permission.ACCESS_DOWNLOAD_MANAGER"/>
     <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
@@ -27,8 +28,8 @@
     <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT"/>
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.WAKE_LOCK"/>
-    <uses-permission android:name="android.permission.READ_HISTORY_BOOKMARKS"/>
-    <uses-permission android:name="android.permission.WRITE_HISTORY_BOOKMARKS"/>
+    <uses-permission android:name="com.android.browser.permission.READ_HISTORY_BOOKMARKS"/>
+    <uses-permission android:name="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS"/>
 
     <application   android:name="Browser"
                    android:label="@string/application_name"
@@ -39,8 +40,8 @@
         <provider android:name="BrowserProvider"
                   android:authorities="browser"
                   android:multiprocess="true"
-                  android:readPermission="android.permission.READ_HISTORY_BOOKMARKS"
-                  android:writePermission="android.permission.WRITE_HISTORY_BOOKMARKS">
+                  android:readPermission="com.android.browser.permission.READ_HISTORY_BOOKMARKS"
+                  android:writePermission="com.android.browser.permission.WRITE_HISTORY_BOOKMARKS">
             <path-permission android:path="/bookmarks/search_suggest_query"
                     android:readPermission="android.permission.GLOBAL_SEARCH" />
         </provider>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0d1c229..b108bbc 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -144,6 +144,8 @@
     <string name="open_bookmark">Open</string>
     <!-- Menu item to remove the currently highlighted bookmark-->
     <string name="remove_bookmark">Delete bookmark</string>
+    <!-- Context menu item to remove a history item from bookmarks -->
+    <string name="remove_from_bookmarks">Remove from bookmarks</string>
     <!-- Menu item to remove the currently highlighted history entry from the list of previously visited sites -->
     <string name="remove_history_item">Remove from history</string>
     <!-- Context menu item for setting the bookmark/history item as the homepage -->
@@ -401,7 +403,7 @@
     <!-- Settings summary -->
     <string name="pref_extras_gears_enable_summary">Applications that extend browser functionality</string>
     <!-- Settings label -->
-    <string name="pref_extras_website_settings">Website Settings</string>
+    <string name="pref_extras_website_settings">Website settings</string>
     <!-- Settings summary -->
     <string name="pref_extras_website_settings_summary">View advanced settings for individual websites</string>
     <!-- Settings label -->
@@ -768,11 +770,11 @@
     <!-- Used as a toast notification after the user close the html5 webstorage permission dialog -->
     <string name="webstorage_notification">The quota for this site can be changed in the Local Storage section of the Browser settings</string>
     <!-- Used in the Browser Settings -->
-    <string name="webstorage_clear_data_title">Clear Stored Data</string>
+    <string name="webstorage_clear_data_title">Clear stored data</string>
     <string name="webstorage_clear_data_summary">Remove all databases associated with this website</string>
     <!-- Confirmation dialog when the user ask to clear all data for an origin -->
-    <string name="webstorage_clear_data_dialog_title">Clear All Data</string>
-    <string name="webstorage_clear_data_dialog_message">All stored data by this origin will be deleted</string>
+    <string name="webstorage_clear_data_dialog_title">Clear stored data</string>
+    <string name="webstorage_clear_data_dialog_message">All data stored by this website will be deleted</string>
     <string name="webstorage_clear_data_dialog_ok_button">Clear All</string>
     <string name="webstorage_clear_data_dialog_cancel_button">Cancel</string>
     <!-- Strings used in the summary of origins -->
diff --git a/src/com/android/browser/AddBookmarkPage.java b/src/com/android/browser/AddBookmarkPage.java
index e827a7e..191659a 100644
--- a/src/com/android/browser/AddBookmarkPage.java
+++ b/src/com/android/browser/AddBookmarkPage.java
@@ -142,7 +142,7 @@
                 setResult(RESULT_OK, (new Intent()).setAction(
                         getIntent().toString()).putExtras(mMap));
             } else {
-                Bookmarks.addBookmark(null, getContentResolver(), url, title);
+                Bookmarks.addBookmark(null, getContentResolver(), url, title, true);
                 setResult(RESULT_OK);
             }
         } catch (IllegalStateException e) {
diff --git a/src/com/android/browser/Bookmarks.java b/src/com/android/browser/Bookmarks.java
index 97e6b20..a3dc919 100644
--- a/src/com/android/browser/Bookmarks.java
+++ b/src/com/android/browser/Bookmarks.java
@@ -47,9 +47,13 @@
      *  @param cr The ContentResolver being used to add the bookmark to the db.
      *  @param url URL of the website to be bookmarked.
      *  @param name Provided name 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.
      */
     /* package */ static void addBookmark(Context context,
-            ContentResolver cr, String url, String name) {
+            ContentResolver cr, String url, String name,
+            boolean retainIcon) {
         // Want to append to the beginning of the list
         long creationTime = new Date().getTime();
         // First we check to see if the user has already visited this
@@ -137,7 +141,9 @@
                 cr.insert(Browser.BOOKMARKS_URI, map);
             }
         }
-        WebIconDatabase.getInstance().retainIconForPageUrl(url);
+        if (retainIcon) {
+            WebIconDatabase.getInstance().retainIconForPageUrl(url);
+        }
         cursor.deactivate();
         if (context != null) {
             Toast.makeText(context, R.string.added_to_bookmarks,
diff --git a/src/com/android/browser/BrowserActivity.java b/src/com/android/browser/BrowserActivity.java
index d148c0a..7f40494 100644
--- a/src/com/android/browser/BrowserActivity.java
+++ b/src/com/android/browser/BrowserActivity.java
@@ -165,8 +165,6 @@
 
     private SensorManager mSensorManager = null;
 
-    private WebStorage.QuotaUpdater mWebStorageQuotaUpdater = null;
-
     // These are single-character shortcuts for searching popular sources.
     private static final int SHORTCUT_INVALID = 0;
     private static final int SHORTCUT_GOOGLE_SEARCH = 1;
@@ -3850,36 +3848,38 @@
         }
 
         /**
-         * The origin has exceeded it's database quota.
+         * 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 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,
+            String databaseIdentifier, long currentQuota, long totalUsedQuota,
             WebStorage.QuotaUpdater quotaUpdater) {
-            if(LOGV_ENABLED) {
-                Log.v(LOGTAG,
-                      "BrowserActivity received onExceededDatabaseQuota for "
-                      + url +
-                      ":"
-                      + databaseIdentifier +
-                      "(current quota: "
-                      + currentQuota +
-                      ")");
-            }
-            mWebStorageQuotaUpdater = quotaUpdater;
-            String DIALOG_PACKAGE = "com.android.browser";
-            String DIALOG_CLASS = DIALOG_PACKAGE + ".PermissionDialog";
-            Intent intent = new Intent();
-            intent.setClassName(DIALOG_PACKAGE, DIALOG_CLASS);
-            intent.putExtra(PermissionDialog.PARAM_ORIGIN, url);
-            intent.putExtra(PermissionDialog.PARAM_QUOTA, currentQuota);
-            startActivityForResult(intent, WEBSTORAGE_QUOTA_DIALOG);
+            mSettings.getWebStorageSizeManager().onExceededDatabaseQuota(
+                    url, databaseIdentifier, currentQuota, 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);
         }
 
         /* Adds a JavaScript error message to the system log.
@@ -4616,14 +4616,6 @@
                     }
                 }
                 break;
-            case WEBSTORAGE_QUOTA_DIALOG:
-                long currentQuota = 0;
-                if (resultCode == RESULT_OK && intent != null) {
-                    currentQuota = intent.getLongExtra(
-                        PermissionDialog.PARAM_QUOTA, currentQuota);
-                }
-                mWebStorageQuotaUpdater.updateQuota(currentQuota);
-                break;
             default:
                 break;
         }
@@ -5169,7 +5161,6 @@
     final static int COMBO_PAGE                 = 1;
     final static int DOWNLOAD_PAGE              = 2;
     final static int PREFERENCES_PAGE           = 3;
-    final static int WEBSTORAGE_QUOTA_DIALOG    = 4;
 
     // the frenquency of checking whether system memory is low
     final static int CHECK_MEMORY_INTERVAL = 30000;     // 30 seconds
diff --git a/src/com/android/browser/BrowserBackupAgent.java b/src/com/android/browser/BrowserBackupAgent.java
index 9e10370..c239b12 100644
--- a/src/com/android/browser/BrowserBackupAgent.java
+++ b/src/com/android/browser/BrowserBackupAgent.java
@@ -25,14 +25,16 @@
 import android.os.ParcelFileDescriptor;
 import android.provider.Browser;
 import android.provider.Browser.BookmarkColumns;
+import android.util.Log;
 
+import java.io.ByteArrayOutputStream;
 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.util.ArrayList;
 import java.util.zip.CRC32;
 
 /**
@@ -40,9 +42,16 @@
  * 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 = "BrowserBookmarkAgent";
+    static final boolean DEBUG = true;
+
     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;
 
     /**
      * In order to determine whether the bookmark set has changed since the
@@ -51,6 +60,7 @@
      *
      * 1. the size of the flattened bookmark file
      * 2. the CRC32 of that file
+     * 3. the agent version number [relevant following an OTA]
      *
      * After we flatten the bookmarks file here in onBackup, we compare its
      * metrics with the values from the saved state.  If they match, it means
@@ -63,6 +73,7 @@
             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(
@@ -70,59 +81,28 @@
         try {
             savedFileSize = in.readLong();
             savedCrc = in.readLong();
+            savedVersion = in.readInt();
         } catch (EOFException e) {
             // It means we had no previous state; that's fine
         }
 
-        // TODO: BUILD THE FLATTENED BOOKMARK FILE FROM THE DB (into tmpfile)
-        File tmpfile = getFilesDir().createTempFile("bkp", null);
-        CRC32 crc = new CRC32();
+        // Build a flattened representation of the bookmarks table
+        File tmpfile = File.createTempFile("bkp", null, getCacheDir());
         try {
-            Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
-                    new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS,
-                    BookmarkColumns.DATE, BookmarkColumns.CREATED,
-                    BookmarkColumns.TITLE },
-                    BookmarkColumns.BOOKMARK + " == 1 ", null, null);
-            int count = cursor.getCount();
-            FileOutputStream out = new FileOutputStream(tmpfile);
-            for (int i = 0; i < count; i++) {
-                StringBuilder sb = new StringBuilder();
-                // URL
-                sb.append("'");
-                sb.append(cursor.getString(0));
-                sb.append("','");
-                // VISITS
-                sb.append(cursor.getInt(1));
-                sb.append("','");
-                // DATE
-                sb.append(cursor.getLong(2));
-                sb.append("','");
-                // CREATED
-                sb.append(cursor.getLong(3));
-                sb.append("','");
-                // TITLE
-                sb.append(cursor.getString(4));
-                sb.append("'");
-                out.write(sb.toString().getBytes());
+            FileOutputStream outfstream = new FileOutputStream(tmpfile);
+            long newCrc = buildBookmarkFile(outfstream);
+            outfstream.close();
 
-                cursor.moveToNext();
-            }
-            out.close();
-            /*
-                    android.util.Log.d("s", "backing up data" +
-                            getContentResolver().openFileDescriptor(Browser.BOOKMARKS_URI, "r").toString());
-             */
-            // NOTE: feed the flattened data through the crc engine on the fly
-            // to save re-reading it later just to checksum it
-
-            // Once the file is built, compare its metrics with the saved ones
-            if ((crc.getValue() != savedCrc) || (tmpfile.length() != savedFileSize)) {
+            // Any changes since the last backup?
+            if ((savedVersion != BACKUP_AGENT_VERSION)
+                    || (newCrc != savedCrc)
+                    || (tmpfile.length() != savedFileSize)) {
                 // Different checksum or different size, so we need to back it up
                 copyFileToBackup(BOOKMARK_KEY, tmpfile, data);
             }
 
-            // Last, record the metrics of the bookmark file that we just stored
-            writeBackupState(tmpfile.length(), crc.getValue(), newState);
+            // Record our backup state and we're done
+            writeBackupState(tmpfile.length(), newCrc, newState);
         } finally {
             // Make sure to tidy up when we're done
             tmpfile.delete();
@@ -138,14 +118,60 @@
     public void onRestore(BackupDataInput data, int appVersionCode,
             ParcelFileDescriptor newState) throws IOException {
         long crc = -1;
-        File tmpfile = getFilesDir().createTempFile("rst", null);
+        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());
 
-                    // TODO: READ THE FLAT BOOKMARKS FILE 'tmpfile' AND REBUILD THE DB TABLE
+                    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();
+                        if (DEBUG) Log.v(TAG, "Restoring " + N + " bookmarks");
+                        String[] urlCol = new String[] { BookmarkColumns.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(Browser.BOOKMARKS_URI,
+                                    urlCol,  BookmarkColumns.URL + " == '" + mark.url + "' AND " +
+                                    BookmarkColumns.BOOKMARK + " == 1 ", null, null);
+                            // if not, insert it
+                            if (cursor.getCount() <= 0) {
+                                Log.v(TAG, "Did not see url: " + mark.url);
+                                // Right now we do not reconstruct the db entry in its
+                                // entirety; we just add a new bookmark with the same data
+                                Bookmarks.addBookmark(null, getContentResolver(),
+                                        mark.url, mark.title, false);
+                            } else {
+                                Log.v(TAG, "Skipping extant url: " + mark.url);
+                            }
+                            cursor.close();
+                        }
+                    } catch (IOException ioe) {
+                        Log.w(TAG, "Bad backup data; not restoring");
+                        crc = -1;
+                    }
                 }
 
                 // Last, write the state we just restored from so we can discern
@@ -158,10 +184,67 @@
         }
     }
 
+    class Bookmark {
+        public String url;
+        public int visits;
+        public long date;
+        public long created;
+        public String title;
+    }
     /*
      * Utility functions
      */
 
+    // Flatten the bookmarks table into the given file, calculating its CRC in the process
+    private long buildBookmarkFile(FileOutputStream outfstream) throws IOException {
+        CRC32 crc = new CRC32();
+        ByteArrayOutputStream bufstream = new ByteArrayOutputStream(512);
+        DataOutputStream bout = new DataOutputStream(bufstream);
+
+        Cursor cursor = getContentResolver().query(Browser.BOOKMARKS_URI,
+                new String[] { BookmarkColumns.URL, BookmarkColumns.VISITS,
+                BookmarkColumns.DATE, BookmarkColumns.CREATED,
+                BookmarkColumns.TITLE },
+                BookmarkColumns.BOOKMARK + " == 1 ", null, null);
+
+        // The first thing in the file is the row count...
+        int count = cursor.getCount();
+        if (DEBUG) Log.v(TAG, "Backing up " + count + " bookmarks");
+        bout.writeInt(count);
+        byte[] record = bufstream.toByteArray();
+        crc.update(record);
+        outfstream.write(record);
+
+        // ... followed by the data for each row
+        for (int i = 0; i < count; i++) {
+            cursor.moveToNext();
+
+            String url = cursor.getString(0);
+            int visits = cursor.getInt(1);
+            long date = cursor.getLong(2);
+            long created = cursor.getLong(3);
+            String title = cursor.getString(4);
+
+            // construct the flattened record in a byte array
+            bufstream.reset();
+            bout.writeUTF(url);
+            bout.writeInt(visits);
+            bout.writeLong(date);
+            bout.writeLong(created);
+            bout.writeUTF(title);
+
+            // Update the CRC and write the record to the temp file
+            record = bufstream.toByteArray();
+            crc.update(record);
+            outfstream.write(record);
+
+            if (DEBUG) Log.v(TAG, "   wrote url " + url);
+        }
+
+        cursor.close();
+        return crc.getValue();
+    }
+
     // Write the file to backup as a single record under the given key
     private void copyFileToBackup(String key, File file, BackupDataOutput data)
             throws IOException {
@@ -187,13 +270,16 @@
         final int CHUNK = 8192;
         byte[] buf = new byte[CHUNK];
         CRC32 crc = new CRC32();
+        FileOutputStream out = new FileOutputStream(file);
 
         while (toRead > 0) {
             int numRead = data.readEntityData(buf, 0, CHUNK);
             crc.update(buf, 0, numRead);
+            out.write(buf, 0, numRead);
             toRead -= numRead;
         }
 
+        out.close();
         return crc.getValue();
     }
 
@@ -204,5 +290,6 @@
                 new FileOutputStream(stateFile.getFileDescriptor()));
         out.writeLong(fileSize);
         out.writeLong(crc);
+        out.writeInt(BACKUP_AGENT_VERSION);
     }
 }
diff --git a/src/com/android/browser/BrowserHistoryPage.java b/src/com/android/browser/BrowserHistoryPage.java
index d5e7049..335d8fe 100644
--- a/src/com/android/browser/BrowserHistoryPage.java
+++ b/src/com/android/browser/BrowserHistoryPage.java
@@ -62,6 +62,7 @@
     private HistoryAdapter          mAdapter;
     private DateSorter              mDateSorter;
     private boolean                 mMaxTabsOpen;
+    private HistoryItem             mContextHeader;
 
     private final static String LOGTAG = "browser";
 
@@ -166,7 +167,7 @@
         }  
         return super.onOptionsItemSelected(item);
     }
-    
+
     @Override
     public void onCreateContextMenu(ContextMenu menu, View v,
             ContextMenuInfo menuInfo) {
@@ -181,12 +182,25 @@
         MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.historycontext, menu);
 
+        HistoryItem historyItem = (HistoryItem) i.targetView;
+
         // Setup the header
-        menu.setHeaderTitle(((HistoryItem)i.targetView).getUrl());
+        if (mContextHeader == null) {
+            mContextHeader = new HistoryItem(this);
+        } else if (mContextHeader.getParent() != null) {
+            ((ViewGroup) mContextHeader.getParent()).removeView(mContextHeader);
+        }
+        historyItem.copyTo(mContextHeader);
+        menu.setHeaderView(mContextHeader);
 
         // Only show open in new tab if we have not maxed out available tabs
         menu.findItem(R.id.new_window_context_menu_id).setVisible(!mMaxTabsOpen);
         
+        // 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 = getPackageManager();
         Intent send = new Intent(Intent.ACTION_SEND);
@@ -201,8 +215,9 @@
     public boolean onContextItemSelected(MenuItem item) {
         ExpandableListContextMenuInfo i = 
             (ExpandableListContextMenuInfo) item.getMenuInfo();
-        String url = ((HistoryItem)i.targetView).getUrl();
-        String title = ((HistoryItem)i.targetView).getName();
+        HistoryItem historyItem = (HistoryItem) i.targetView;
+        String url = historyItem.getUrl();
+        String title = historyItem.getName();
         switch (item.getItemId()) {
             case R.id.open_context_menu_id:
                 loadUrl(url, false);
@@ -211,7 +226,12 @@
                 loadUrl(url, true);
                 return true;
             case R.id.save_to_bookmarks_menu_id:
-                Browser.saveBookmark(this, title, url);
+                if (historyItem.isBookmark()) {
+                    Bookmarks.removeFromBookmarks(this, getContentResolver(),
+                            url);
+                } else {
+                    Browser.saveBookmark(this, title, url);
+                }
                 return true;
             case R.id.share_link_context_menu_id:
                 Browser.sendString(this, url);
diff --git a/src/com/android/browser/BrowserProvider.java b/src/com/android/browser/BrowserProvider.java
index 29c65e8..b74c9eb 100644
--- a/src/com/android/browser/BrowserProvider.java
+++ b/src/com/android/browser/BrowserProvider.java
@@ -19,6 +19,7 @@
 import com.google.android.providers.GoogleSettings.Partner;
 
 import android.app.SearchManager;
+import android.backup.BackupManager;
 import android.content.ComponentName;
 import android.content.ContentProvider;
 import android.content.ContentUris;
@@ -54,6 +55,7 @@
 public class BrowserProvider extends ContentProvider {
 
     private SQLiteOpenHelper mOpenHelper;
+    private BackupManager mBackupManager;
     private static final String sDatabaseName = "browser.db";
     private static final String TAG = "BrowserProvider";
     private static final String ORDER_BY = "visits DESC, date DESC";
@@ -264,6 +266,7 @@
     public boolean onCreate() {
         final Context context = getContext();
         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.
@@ -736,6 +739,7 @@
 
     @Override
     public Uri insert(Uri url, ContentValues initialValues) {
+        boolean isBookmarkTable = false;
         SQLiteDatabase db = mOpenHelper.getWritableDatabase();
 
         int match = URI_MATCHER.match(url);
@@ -749,6 +753,7 @@
                     uri = ContentUris.withAppendedId(Browser.BOOKMARKS_URI,
                             rowID);
                 }
+                isBookmarkTable = true;
                 break;
             }
 
@@ -771,6 +776,11 @@
             throw new IllegalArgumentException("Unknown URL");
         }
         getContext().getContentResolver().notifyChange(uri, null);
+
+        // back up the new bookmark set if we just inserted one
+        if (isBookmarkTable) {
+            mBackupManager.dataChanged();
+        }
         return uri;
     }
 
@@ -783,7 +793,10 @@
             throw new IllegalArgumentException("Unknown URL");
         }
 
-        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+        // need to know whether it's the bookmarks table for a couple fo reasons
+        boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
+
+        if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
             StringBuilder sb = new StringBuilder();
             if (where != null && where.length() > 0) {
                 sb.append("( ");
@@ -797,6 +810,11 @@
 
         int count = db.delete(TABLE_NAMES[match % 10], where, whereArgs);
         getContext().getContentResolver().notifyChange(url, null);
+
+        // back up the new bookmark set if we just deleted one
+        if (isBookmarkTable) {
+            mBackupManager.dataChanged();
+        }
         return count;
     }
 
@@ -810,7 +828,9 @@
             throw new IllegalArgumentException("Unknown URL");
         }
 
-        if (match == URI_MATCH_BOOKMARKS_ID || match == URI_MATCH_SEARCHES_ID) {
+        boolean isBookmarkTable = (match == URI_MATCH_BOOKMARKS_ID);
+
+        if (isBookmarkTable || match == URI_MATCH_SEARCHES_ID) {
             StringBuilder sb = new StringBuilder();
             if (where != null && where.length() > 0) {
                 sb.append("( ");
@@ -824,6 +844,11 @@
 
         int ret = db.update(TABLE_NAMES[match % 10], values, where, whereArgs);
         getContext().getContentResolver().notifyChange(url, null);
+
+        // back up the new bookmark set if we just changed one
+        if (isBookmarkTable) {
+            mBackupManager.dataChanged();
+        }
         return ret;
     }
 
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index d2371cd..3074d2b 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -25,7 +25,6 @@
 import android.content.SharedPreferences.Editor;
 import android.preference.PreferenceActivity;
 import android.preference.PreferenceScreen;
-import android.os.StatFs;
 import android.webkit.CookieManager;
 import android.webkit.WebView;
 import android.webkit.WebViewDatabase;
@@ -79,6 +78,7 @@
     private boolean appCacheEnabled = true;
     private String appCachePath;  // default value set in loadFromDb().
     private long appCacheMaxSize = Long.MAX_VALUE;
+    private WebStorageSizeManager webStorageSizeManager;
     private boolean domStorageEnabled = true;
     private String jsFlags = "";
 
@@ -238,7 +238,9 @@
         // Set the default value for the Application Caches path.
         appCachePath = ctx.getDir("appcache", 0).getPath();
         // Determine the maximum size of the application cache.
-        appCacheMaxSize = getAppCacheMaxSize();
+        webStorageSizeManager = WebStorageSizeManager.getInstance(appCachePath,
+                ctx);
+        appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize();
         // Set the default value for the Database path.
         databasePath = ctx.getDir("databases", 0).getPath();
 
@@ -363,6 +365,10 @@
         return jsFlags;
     }
 
+    public WebStorageSizeManager getWebStorageSizeManager() {
+        return webStorageSizeManager;
+    }
+
     public void setHomePage(Context context, String url) {
         Editor ed = PreferenceManager.
                 getDefaultSharedPreferences(context).edit();
@@ -532,6 +538,8 @@
                 true);
         // reset homeUrl
         setHomePage(ctx, getFactoryResetHomeUrl(ctx));
+        // reset appcache max size
+        appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize();
     }
 
     private String getFactoryResetHomeUrl(Context context) {
@@ -543,35 +551,6 @@
         return url;
     }
 
-    private long getAppCacheMaxSize() {
-        StatFs dataPartition = new StatFs(appCachePath);
-        long freeSpace = dataPartition.getAvailableBlocks()
-            * dataPartition.getBlockSize();
-        long fileSystemSize = dataPartition.getBlockCount()
-            * dataPartition.getBlockSize();
-        return calculateAppCacheMaxSize(fileSystemSize, freeSpace);
-    }
-
-    /*package*/ static long calculateAppCacheMaxSize(long fileSystemSizeBytes,
-            long freeSpaceBytes) {
-        if (fileSystemSizeBytes <= 0
-                || freeSpaceBytes <= 0
-                || freeSpaceBytes > fileSystemSizeBytes) {
-            return 0;
-        }
-
-        long fileSystemSizeRatio =
-            4 << ((int) Math.floor(Math.log10(fileSystemSizeBytes / (1024 * 1024))));
-        long maxSizeBytes = (long) Math.min(Math.floor(fileSystemSizeBytes / fileSystemSizeRatio),
-                Math.floor(freeSpaceBytes / 4));
-        // Round maxSizeBytes up to a multiple of 512KB (except when freeSpaceBytes < 1MB).
-        long maxSizeStepBytes = 512 * 1024;
-        if (freeSpaceBytes < maxSizeStepBytes * 2) {
-            return 0;
-        }
-        return (maxSizeStepBytes * ((maxSizeBytes / maxSizeStepBytes) + 1));
-    }
-
     // Private constructor that does nothing.
     private BrowserSettings() {
     }
diff --git a/src/com/android/browser/HistoryItem.java b/src/com/android/browser/HistoryItem.java
index b37a3bd..e8f15b1 100644
--- a/src/com/android/browser/HistoryItem.java
+++ b/src/com/android/browser/HistoryItem.java
@@ -46,7 +46,7 @@
                     boolean isChecked) {
                 if (isChecked) {
                     Bookmarks.addBookmark(mContext,
-                            mContext.getContentResolver(), mUrl, getName());
+                            mContext.getContentResolver(), mUrl, getName(), true);
                 } else {
                     Bookmarks.removeFromBookmarks(mContext,
                             mContext.getContentResolver(), mUrl);
@@ -55,7 +55,7 @@
         };
     }
     
-    void copyTo(HistoryItem item) {
+    /* package */ void copyTo(HistoryItem item) {
         item.mTextView.setText(mTextView.getText());
         item.mUrlText.setText(mUrlText.getText());
         item.setIsBookmark(mStar.isChecked());
@@ -63,10 +63,17 @@
     }
 
     /**
+     * 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.
      */
-    void setIsBookmark(boolean isBookmark) {
+    /* package */ void setIsBookmark(boolean isBookmark) {
         mStar.setOnCheckedChangeListener(null);
         mStar.setChecked(isBookmark);
         mStar.setOnCheckedChangeListener(mListener);
diff --git a/src/com/android/browser/PermissionDialog.java b/src/com/android/browser/PermissionDialog.java
deleted file mode 100644
index b71261a..0000000
--- a/src/com/android/browser/PermissionDialog.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * 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.Dialog;
-import android.content.Intent;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.KeyEvent;
-import android.view.View;
-import android.view.Window;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-/**
- * Permission dialog for HTML5
- * @hide
- */
-public class PermissionDialog extends Activity {
-
-    private static final String TAG = "PermissionDialog";
-    public static final String PARAM_ORIGIN = "origin";
-    public static final String PARAM_QUOTA = "quota";
-
-    private String mWebStorageOrigin;
-    private long mWebStorageQuota = 0;
-    private int mNotification = 0;
-
-    @Override
-    public void onCreate(Bundle icicle) {
-        super.onCreate(icicle);
-        getParameters();
-        setupDialog();
-    }
-
-    private void getParameters() {
-        Intent intent = getIntent();
-        mWebStorageOrigin = intent.getStringExtra(PARAM_ORIGIN);
-        mWebStorageQuota = intent.getLongExtra(PARAM_QUOTA, 0);
-    }
-
-    private void setupDialog() {
-        requestWindowFeature(Window.FEATURE_NO_TITLE);
-        setContentView(R.layout.permission_dialog);
-
-        setIcon(R.id.icon, android.R.drawable.ic_popup_disk_full);
-        setText(R.id.dialog_title, R.string.query_storage_quota_prompt);
-        setText(R.id.dialog_message, R.string.query_storage_quota_message);
-        setCharSequence(R.id.origin, mWebStorageOrigin);
-
-        setupButton(R.id.button_allow, R.string.permission_button_allow,
-            new View.OnClickListener() {
-                public void onClick(View v) { allow(); }
-            });
-        setupButton(R.id.button_alwaysdeny, R.string.permission_button_alwaysdeny,
-            new View.OnClickListener() {
-                public void onClick(View v) { alwaysdeny(); }
-            });
-        setupButton(R.id.button_deny, R.string.permission_button_deny,
-            new View.OnClickListener() {
-                public void onClick(View v) { deny(); }
-            });
-    }
-
-    private void setText(int viewID, int stringID) {
-        setCharSequence(viewID, getString(stringID));
-    }
-
-    private void setCharSequence(int viewID, CharSequence string) {
-        View view = findViewById(viewID);
-        if (view == null) {
-            return;
-        }
-        view.setVisibility(View.VISIBLE);
-        TextView textView = (TextView) view;
-        textView.setText(string);
-    }
-
-    private void setIcon(int viewID, int imageID) {
-        View view = findViewById(viewID);
-        if (view == null) {
-            return;
-        }
-        view.setVisibility(View.VISIBLE);
-        ImageView icon = (ImageView) view;
-        icon.setImageResource(imageID);
-    }
-
-    private void setupButton(int viewID, int stringID,
-                             View.OnClickListener listener) {
-        View view = findViewById(viewID);
-        if (view == null) {
-            return;
-        }
-        setText(viewID, stringID);
-        view.setOnClickListener(listener);
-    }
-
-    private void useNextQuota() {
-        CharSequence[] values = getResources().getTextArray(
-            R.array.webstorage_quota_entries_values);
-        for (int i=0; i<values.length; i++) {
-            long value = Long.parseLong(values[i].toString());
-            value *= (1024 * 1024); // the string array is expressed in MB
-            if (value > mWebStorageQuota) {
-                mWebStorageQuota = value;
-                break;
-            }
-        }
-    }
-
-    private void allow() {
-        // If somehow there is no "next quota" in the ladder,
-        // we'll add 1MB anyway.
-        mWebStorageQuota += 1024*1024;
-        useNextQuota();
-        mNotification = R.string.webstorage_notification;
-        closeDialog();
-    }
-
-    private void alwaysdeny() {
-        // Setting the quota to 0 will prevent any new data to be
-        // added, but the existing data will not be deleted.
-        mWebStorageQuota = 0;
-        mNotification = R.string.webstorage_notification;
-        closeDialog();
-    }
-
-    private void deny() {
-        closeDialog();
-    }
-
-    private void closeDialog() {
-        Intent intent = new Intent();
-        intent.putExtra(PARAM_QUOTA, mWebStorageQuota);
-        setResult(RESULT_OK, intent);
-        showToast();
-        finish();
-    }
-
-    private void showToast() {
-        if (mNotification != 0) {
-            Toast toast = Toast.makeText(this, mNotification, Toast.LENGTH_LONG);
-            toast.setGravity(Gravity.BOTTOM, 0, 0);
-            toast.show();
-        }
-    }
-
-    public boolean dispatchKeyEvent(KeyEvent event) {
-        if ((event.getKeyCode() == KeyEvent.KEYCODE_BACK)
-              && (event.getAction() == KeyEvent.ACTION_DOWN)) {
-            closeDialog();
-            return true; // event consumed
-        }
-        return super.dispatchKeyEvent(event);
-    }
-
-}
diff --git a/src/com/android/browser/WebStorageSizeManager.java b/src/com/android/browser/WebStorageSizeManager.java
new file mode 100644
index 0000000..e524f4c
--- /dev/null
+++ b/src/com/android/browser/WebStorageSizeManager.java
@@ -0,0 +1,282 @@
+/*
+ * 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.os.StatFs;
+import android.util.Log;
+import android.webkit.WebStorage;
+
+import java.io.File;
+import java.util.Set;
+
+
+/**
+ * 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 min(ORIGIN_DEFAULT_QUOTA, unused_quota).
+ * Quota increases are done in steps, where the increase step is
+ * min(QUOTA_INCREASE_STEP, unused_quota).
+ *
+ * This approach has the drawback that space may remain unused if there
+ * are many websites that store a lot less content than ORIGIN_DEFAULT_QUOTA.
+ * We deal with this by picking a value for ORIGIN_DEFAULT_QUOTA that is smaller
+ * than what the HTML 5 spec recommends. At the same time, picking a very small
+ * value for ORIGIN_DEFAULT_QUOTA may create performance problems since it's
+ * more likely for origins to have to rollback and restart transactions as a
+ * result of reaching the quota more often.
+ *
+ * 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.
+ * TODO(andreip): implement the notification.
+ */
+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.
+    private final static long ORIGIN_DEFAULT_QUOTA = 4 * 1024 * 1024;  // 4MB
+    // The default value for quota increases.
+    private final static long QUOTA_INCREASE_STEP = 2 * 1024 * 1024;  // 2MB
+    // The name of the application cache file. Keep in sync with
+    // WebCore/loader/appcache/ApplicationCacheStorage.cpp
+    private final static String APPCACHE_FILE = "ApplicationCache.db";
+    // The WebStorageSizeManager singleton.
+    private static WebStorageSizeManager mManager;
+    // The application context.
+    private Context mContext;
+    // The global Web storage limit.
+    private long mGlobalLimit;
+    // The maximum size of the application cache file.
+    private long mAppCacheMaxSize;
+
+    /**
+     * Factory method.
+     * @param path is a path on the partition where the app cache data is kept.
+     * @param ctx is the browser application context.
+     * @param storage is the WebStorage singleton.
+     *
+     */
+    public static WebStorageSizeManager getInstance(String appCachePath,
+            Context ctx) {
+       if (mManager == null) {
+           mManager = new WebStorageSizeManager(appCachePath, ctx);
+       }
+       return mManager;
+    }
+
+    /**
+     * 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 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 totalUsedQuota,
+        WebStorage.QuotaUpdater quotaUpdater) {
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG,
+                  "Received onExceededDatabaseQuota for "
+                  + url
+                  + ":"
+                  + databaseIdentifier
+                  + "(current quota: "
+                  + currentQuota
+                  + ")");
+        }
+        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
+
+        if (totalUnusedQuota < QUOTA_INCREASE_STEP) {
+            // There definitely isn't any more space. Fire notifications
+            // and exit.
+            scheduleOutOfSpaceNotification();
+            quotaUpdater.updateQuota(currentQuota);
+            if(LOGV_ENABLED) {
+                Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
+            }
+            return;
+        }
+        // We have enough space inside mGlobalLimit.
+        long newOriginQuota = currentQuota;
+        if (newOriginQuota == 0) {
+            // This is a new origin. It wants an initial quota. It is guaranteed
+            // to get at least ORIGIN_INCREASE_STEP bytes.
+            newOriginQuota =
+                Math.min(ORIGIN_DEFAULT_QUOTA, totalUnusedQuota);
+        } else {
+            // This is an origin we have seen before. It wants a quota
+            // increase.
+            newOriginQuota +=
+                Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
+        }
+        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) {
+            // There definitely isn't any more space. Fire notifications
+            // and exit.
+            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;
+        quotaUpdater.updateQuota(mAppCacheMaxSize);
+
+        if(LOGV_ENABLED) {
+            Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
+                    + mAppCacheMaxSize);
+        }
+    }
+
+    // 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(String path) {
+        StatFs dataPartition = new StatFs(path);
+        long freeSpace = dataPartition.getAvailableBlocks()
+            * dataPartition.getBlockSize();
+        long fileSystemSize = dataPartition.getBlockCount()
+            * dataPartition.getBlockSize();
+        return calculateGlobalLimit(fileSystemSize, freeSpace);
+    }
+
+    // Returns the current size (in bytes) of the application cache file.
+    private long getCurrentAppCacheSize(String path) {
+        File file = new File(path + File.separator + APPCACHE_FILE);
+        return file.length();
+    }
+
+    /*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() {
+        // TODO(andreip): implement.
+    }
+    // Private ctor.
+    private WebStorageSizeManager(String appCachePath, Context ctx) {
+        mContext = ctx;
+        mGlobalLimit = getGlobalLimit(appCachePath);
+        // 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,
+                getCurrentAppCacheSize(appCachePath));
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/browser/WebsiteSettingsActivity.java b/src/com/android/browser/WebsiteSettingsActivity.java
index c9042cd..f91879f 100644
--- a/src/com/android/browser/WebsiteSettingsActivity.java
+++ b/src/com/android/browser/WebsiteSettingsActivity.java
@@ -40,6 +40,7 @@
 import android.widget.TextView;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Set;
 import java.util.Vector;
@@ -59,10 +60,10 @@
         private String mTitle;
         private Bitmap mIcon;
 
-        public Site(String origin, String title, Bitmap icon) {
+        public Site(String origin) {
             mOrigin = origin;
-            mTitle = title;
-            mIcon = icon;
+            mTitle = null;
+            mIcon = null;
         }
 
         public String getOrigin() {
@@ -73,10 +74,6 @@
             mTitle = title;
         }
 
-        public String getTitle() {
-            return mTitle;
-        }
-
         public void setIcon(Bitmap icon) {
             mIcon = icon;
         }
@@ -84,6 +81,19 @@
         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;
+        }
     }
 
     class SiteAdapter extends ArrayAdapter<Site>
@@ -107,19 +117,36 @@
             clear();
 
             // Get the list of origins we want to display
-            HashMap<String, Site> uris = new HashMap<String, Site>();
             Set origins = WebStorage.getInstance().getOrigins();
+            Set sites = new HashSet<Site>();
             if (origins != null) {
                 Iterator<String> iter = origins.iterator();
                 while (iter.hasNext()) {
                     String origin = iter.next();
-                    Site site = new Site(origin, origin, null);
-                    uris.put(Uri.parse(origin).getHost(), site);
+                    Site site = new Site(origin);
+                    sites.add(site);
                 }
             }
 
-            // Check the bookmark db -- if one of our origin matches,
-            // we set its title and favicon
+            // Create a map from host to origin. This is used to add metadata
+            // (title, icon) for this origin from the bookmarks DB.
+            HashMap hosts = new HashMap<String, Set<Site> >();
+            Iterator<Site> sitesIter = sites.iterator();
+            while (sitesIter.hasNext()) {
+                Site site = sitesIter.next();
+                String host = Uri.parse(site.getOrigin()).getHost();
+                Set hostSites = null;
+                if (hosts.containsKey(host)) {
+                    hostSites = (Set) 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 = getContext().getContentResolver().query(Browser.BOOKMARKS_URI,
                     new String[] { Browser.BookmarkColumns.URL, Browser.BookmarkColumns.TITLE,
                     Browser.BookmarkColumns.FAVICON }, "bookmark = 1", null, null);
@@ -131,13 +158,18 @@
                 do {
                     String url = c.getString(urlIndex);
                     String host = Uri.parse(url).getHost();
-                    if (uris.containsKey(host)) {
+                    if (hosts.containsKey(host)) {
                         String title = c.getString(titleIndex);
-                        Site site = uris.get(host);
-                        site.setTitle(title);
+                        Bitmap bmp = null;
                         byte[] data = c.getBlob(faviconIndex);
                         if (data != null) {
-                            Bitmap bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
+                            bmp = BitmapFactory.decodeByteArray(data, 0, data.length);
+                        }
+                        Set matchingSites = (Set) hosts.get(host);
+                        sitesIter = matchingSites.iterator();
+                        while (sitesIter.hasNext()) {
+                            Site site = sitesIter.next();
+                            site.setTitle(title);
                             if (bmp != null) {
                                 site.setIcon(bmp);
                             }
@@ -147,11 +179,9 @@
             }
 
             // We can now simply populate our array with Site instances
-            Set keys = uris.keySet();
-            Iterator iter = keys.iterator();
-            while (iter.hasNext()) {
-                String origin = (String) iter.next();
-                Site site = uris.get(origin);
+            sitesIter = sites.iterator();
+            while (sitesIter.hasNext()) {
+                Site site = sitesIter.next();
                 add(site);
             }
 
@@ -211,8 +241,8 @@
 
             if (mCurrentSite == null) {
                 Site site = getItem(position);
-                title.setText(site.getTitle());
-                subtitle.setText(site.getOrigin());
+                title.setText(site.getPrettyTitle());
+                subtitle.setText(site.getPrettyOrigin());
                 icon.setVisibility(View.VISIBLE);
                 Bitmap bmp = site.getIcon();
                 if (bmp == null) {
diff --git a/tests/src/com/android/browser/BrowserSettingsUnitTests.java b/tests/src/com/android/browser/BrowserSettingsUnitTests.java
deleted file mode 100644
index fdaa5c8..0000000
--- a/tests/src/com/android/browser/BrowserSettingsUnitTests.java
+++ /dev/null
@@ -1,88 +0,0 @@
-/*
- * 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.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
-
-/**
- * This is a series of unit tests for the BrowserSettings class.
- *
- */
-@MediumTest
-public class BrowserSettingsUnitTests extends AndroidTestCase {
-
-    /**
-     * Test the application caches max size calculator.
-     */
-    public void testCalculateAppCacheMaxSize() {
-        long fileSystemSize = 78643200;  // 75 MB
-        long freeSpaceSize = 25165824;  // 24 MB
-        long maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(6815744, maxSize);  // 6.5MB
-
-        fileSystemSize = 78643200;  // 75 MB
-        freeSpaceSize = 60 * 1024 * 1024;  // 60MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(9961472, maxSize);  // 9.5MB
-
-        fileSystemSize = 8589934592L;  // 8 GB
-        freeSpaceSize = 4294967296L;  // 4 GB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(268959744L, maxSize);  // 256.5 MB
-
-        fileSystemSize = -14;
-        freeSpaceSize = 21;
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(0, maxSize);
-
-        fileSystemSize = 100;
-        freeSpaceSize = 101;
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(0, maxSize);
-
-        fileSystemSize = 3774873; // ~4.2 MB
-        freeSpaceSize = 2560000;  // ~2.4 MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(1048576, maxSize);  // 1 MB
-
-        fileSystemSize = 4404019; // ~4.2 MB
-        freeSpaceSize = 3774873;  // ~3.6 MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(1048576, maxSize);  // 1 MB
-
-        fileSystemSize = 4404019; // ~4.2 MB
-        freeSpaceSize = 4404019;  // ~4.2 MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(1572864, maxSize);  // 1.5 MB
-
-        fileSystemSize = 1048576; // 1 MB
-        freeSpaceSize = 1048575;  // 1 MB - 1 byte
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(0, maxSize);
-
-        fileSystemSize = 1048576; // 1 MB
-        freeSpaceSize = 1048576;  // 1 MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(524288, maxSize);  // 512KB
-
-        fileSystemSize = 3774873; // ~3.6 MB
-        freeSpaceSize = 2097151;  // 2 MB - 1 byte
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(524288, maxSize);  // 512KB
-    }
-}
diff --git a/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java b/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
new file mode 100644
index 0000000..4ba758d
--- /dev/null
+++ b/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
@@ -0,0 +1,88 @@
+/*
+ * 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.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.MediumTest;
+
+/**
+ * This is a series of unit tests for the WebStorageSizeManager class.
+ *
+ */
+@MediumTest
+public class WebStorageSizeManagerUnitTests extends AndroidTestCase {
+
+    /**
+     * Test the application caches max size calculator.
+     */
+    public void testCalculateGlobalLimit() {
+        long fileSystemSize = 78643200;  // 75 MB
+        long freeSpaceSize = 25165824;  // 24 MB
+        long maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(12582912, maxSize);  // 12MB
+
+        fileSystemSize = 78643200;  // 75 MB
+        freeSpaceSize = 60 * 1024 * 1024;  // 60MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(19922944, maxSize);  // 19MB
+
+        fileSystemSize = 8589934592L;  // 8 GB
+        freeSpaceSize = 4294967296L;  // 4 GB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(536870912L, maxSize);  // 512 MB
+
+        fileSystemSize = -14;
+        freeSpaceSize = 21;
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 100;
+        freeSpaceSize = 101;
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 3774873; // ~4.2 MB
+        freeSpaceSize = 2560000;  // ~2.4 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(2097152, maxSize);  // 2 MB
+
+        fileSystemSize = 4404019; // ~4.2 MB
+        freeSpaceSize = 3774873;  // ~3.6 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(2097152, maxSize);  // 2 MB
+
+        fileSystemSize = 4404019; // ~4.2 MB
+        freeSpaceSize = 4404019;  // ~4.2 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(3145728, maxSize);  // 3 MB
+
+        fileSystemSize = 1048576; // 1 MB
+        freeSpaceSize = 1048575;  // 1 MB - 1 byte
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 3774873; // ~3.6 MB
+        freeSpaceSize = 2097151;  // 2 MB - 1 byte
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+
+        fileSystemSize = 3774873; // ~3.6 MB
+        freeSpaceSize = 2097151;  // 2 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(0, maxSize);
+    }
+}