OSDN Git Service

Refactor the WebStorage size management:
authorAndrei Popescu <andreip@google.com>
Mon, 27 Jul 2009 11:01:59 +0000 (12:01 +0100)
committerAndrei Popescu <andreip@google.com>
Wed, 29 Jul 2009 10:08:24 +0000 (11:08 +0100)
- Abandon the Quota UI: it does not make sense to ask the users to decide individual database quota increases. It is unlikely anyone will be able to make a meaningul decision.
- Introduce a global limit for all WebStorage content. This is shared between Database and AppCache.
- Make the quota increase decision automatic
- Treat out-of-space situations by creaying a system notification (TODO).

src/com/android/browser/BrowserActivity.java
src/com/android/browser/BrowserSettings.java
src/com/android/browser/PermissionDialog.java [deleted file]
src/com/android/browser/WebStorageSizeManager.java [new file with mode: 0644]
tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java [moved from tests/src/com/android/browser/BrowserSettingsUnitTests.java with 52% similarity]

index d148c0a..7f40494 100644 (file)
@@ -165,8 +165,6 @@ public class BrowserActivity extends Activity
 
     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 @@ public class BrowserActivity extends Activity
         }
 
         /**
-         * 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 @@ public class BrowserActivity extends Activity
                     }
                 }
                 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 @@ public class BrowserActivity extends Activity
     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
index d2371cd..3074d2b 100644 (file)
@@ -25,7 +25,6 @@ import android.content.SharedPreferences;
 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 @@ class BrowserSettings extends Observable {
     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 @@ class BrowserSettings extends Observable {
         // 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 @@ class BrowserSettings extends Observable {
         return jsFlags;
     }
 
+    public WebStorageSizeManager getWebStorageSizeManager() {
+        return webStorageSizeManager;
+    }
+
     public void setHomePage(Context context, String url) {
         Editor ed = PreferenceManager.
                 getDefaultSharedPreferences(context).edit();
@@ -532,6 +538,8 @@ class BrowserSettings extends Observable {
                 true);
         // reset homeUrl
         setHomePage(ctx, getFactoryResetHomeUrl(ctx));
+        // reset appcache max size
+        appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize();
     }
 
     private String getFactoryResetHomeUrl(Context context) {
@@ -543,35 +551,6 @@ class BrowserSettings extends Observable {
         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/PermissionDialog.java b/src/com/android/browser/PermissionDialog.java
deleted file mode 100644 (file)
index b71261a..0000000
+++ /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 (file)
index 0000000..e524f4c
--- /dev/null
@@ -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
@@ -20,69 +20,69 @@ import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
 /**
- * This is a series of unit tests for the BrowserSettings class.
+ * This is a series of unit tests for the WebStorageSizeManager class.
  *
  */
 @MediumTest
-public class BrowserSettingsUnitTests extends AndroidTestCase {
+public class WebStorageSizeManagerUnitTests extends AndroidTestCase {
 
     /**
      * Test the application caches max size calculator.
      */
-    public void testCalculateAppCacheMaxSize() {
+    public void testCalculateGlobalLimit() {
         long fileSystemSize = 78643200;  // 75 MB
         long freeSpaceSize = 25165824;  // 24 MB
-        long maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(6815744, maxSize);  // 6.5MB
+        long maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(12582912, maxSize);  // 12MB
 
         fileSystemSize = 78643200;  // 75 MB
         freeSpaceSize = 60 * 1024 * 1024;  // 60MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(9961472, maxSize);  // 9.5MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(19922944, maxSize);  // 19MB
 
         fileSystemSize = 8589934592L;  // 8 GB
         freeSpaceSize = 4294967296L;  // 4 GB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(268959744L, maxSize);  // 256.5 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(536870912L, maxSize);  // 512 MB
 
         fileSystemSize = -14;
         freeSpaceSize = 21;
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
         assertEquals(0, maxSize);
 
         fileSystemSize = 100;
         freeSpaceSize = 101;
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(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
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(2097152, maxSize);  // 2 MB
 
         fileSystemSize = 4404019; // ~4.2 MB
         freeSpaceSize = 3774873;  // ~3.6 MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(1048576, maxSize);  // 1 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(2097152, maxSize);  // 2 MB
 
         fileSystemSize = 4404019; // ~4.2 MB
         freeSpaceSize = 4404019;  // ~4.2 MB
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
-        assertEquals(1572864, maxSize);  // 1.5 MB
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(fileSystemSize, freeSpaceSize);
+        assertEquals(3145728, maxSize);  // 3 MB
 
         fileSystemSize = 1048576; // 1 MB
         freeSpaceSize = 1048575;  // 1 MB - 1 byte
-        maxSize = BrowserSettings.calculateAppCacheMaxSize(fileSystemSize, freeSpaceSize);
+        maxSize = WebStorageSizeManager.calculateGlobalLimit(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
+        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);
     }
 }