Add unit tests for the WebStorageSizemanager. Also reduce the default quota and quota increase step
diff --git a/src/com/android/browser/BrowserSettings.java b/src/com/android/browser/BrowserSettings.java
index 3074d2b..79deb61 100644
--- a/src/com/android/browser/BrowserSettings.java
+++ b/src/com/android/browser/BrowserSettings.java
@@ -238,8 +238,10 @@
         // Set the default value for the Application Caches path.
         appCachePath = ctx.getDir("appcache", 0).getPath();
         // Determine the maximum size of the application cache.
-        webStorageSizeManager = WebStorageSizeManager.getInstance(appCachePath,
-                ctx);
+        webStorageSizeManager = new WebStorageSizeManager(
+                ctx,
+                new WebStorageSizeManager.StatFsDiskInfo(appCachePath),
+                new WebStorageSizeManager.WebKitAppCacheInfo(appCachePath));
         appCacheMaxSize = webStorageSizeManager.getAppCacheMaxSize();
         // Set the default value for the Database path.
         databasePath = ctx.getDir("databases", 0).getPath();
diff --git a/src/com/android/browser/WebStorageSizeManager.java b/src/com/android/browser/WebStorageSizeManager.java
index e524f4c..ff21ea9 100644
--- a/src/com/android/browser/WebStorageSizeManager.java
+++ b/src/com/android/browser/WebStorageSizeManager.java
@@ -92,34 +92,98 @@
     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
+    public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024;  // 3MB
     // 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;
+    public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024;  // 1MB
     // The application context.
-    private Context mContext;
+    private final Context mContext;
     // The global Web storage limit.
-    private long mGlobalLimit;
+    private final 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.
-     *
+     * Interface used by the WebStorageSizeManager to obtain information
+     * about the underlying file system. This functionality is separated
+     * into its own interface mainly for testing purposes.
      */
-    public static WebStorageSizeManager getInstance(String appCachePath,
-            Context ctx) {
-       if (mManager == null) {
-           mManager = new WebStorageSizeManager(appCachePath, ctx);
-       }
-       return mManager;
+    public interface DiskInfo {
+        /**
+         * @return the size of the free space in the file system.
+         */
+        public long getFreeSpaceSizeBytes();
+
+        /**
+         * @return the total size of the file system.
+         */
+        public long getTotalSizeBytes();
+    };
+
+    private DiskInfo mDiskInfo;
+    // For convenience, we provide a DiskInfo implementation that uses StatFs.
+    public static class StatFsDiskInfo implements DiskInfo {
+        private StatFs mFs;
+
+        public StatFsDiskInfo(String path) {
+            mFs = new StatFs(path);
+        }
+
+        public long getFreeSpaceSizeBytes() {
+            return mFs.getAvailableBlocks() * mFs.getBlockSize();
+        }
+
+        public long getTotalSizeBytes() {
+            return mFs.getBlockCount() * mFs.getBlockSize();
+        }
+    };
+
+    /**
+     * Interface used by the WebStorageSizeManager to obtain information
+     * about the appcache file. This functionality is separated into its own
+     * interface mainly for testing purposes.
+     */
+    public interface AppCacheInfo {
+        /**
+         * @return the current size of the appcache file.
+         */
+        public long getAppCacheSizeBytes();
+    };
+
+    // For convenience, we provide an AppCacheInfo implementation.
+    public static class WebKitAppCacheInfo implements AppCacheInfo {
+        // The name of the application cache file. Keep in sync with
+        // WebCore/loader/appcache/ApplicationCacheStorage.cpp
+        private final static String APPCACHE_FILE = "ApplicationCache.db";
+        private String mAppCachePath;
+
+        public WebKitAppCacheInfo(String path) {
+            mAppCachePath = path;
+        }
+
+        public long getAppCacheSizeBytes() {
+            File file = new File(mAppCachePath
+                    + File.separator
+                    + APPCACHE_FILE);
+            return file.length();
+        }
+    };
+
+    /**
+     * Public ctor
+     * @param ctx is the application context
+     * @param diskInfo is the DiskInfo instance used to query the file system.
+     * @param appCacheInfo is the AppCacheInfo used to query info about the
+     * appcache file.
+     */
+    public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
+            AppCacheInfo appCacheInfo) {
+        mContext = ctx;
+        mDiskInfo = diskInfo;
+        mGlobalLimit = getGlobalLimit();
+        // The initial max size of the app cache is either 25% of the global
+        // limit or the current size of the app cache file, whichever is bigger.
+        mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
+                appCacheInfo.getAppCacheSizeBytes());
     }
 
     /**
@@ -150,11 +214,13 @@
                   + databaseIdentifier
                   + "(current quota: "
                   + currentQuota
+                  + ", total used quota: "
+                  + totalUsedQuota
                   + ")");
         }
         long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
 
-        if (totalUnusedQuota < QUOTA_INCREASE_STEP) {
+        if (totalUnusedQuota <= 0) {
             // There definitely isn't any more space. Fire notifications
             // and exit.
             scheduleOutOfSpaceNotification();
@@ -167,8 +233,7 @@
         // 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.
+            // This is a new origin. It wants an initial quota.
             newOriginQuota =
                 Math.min(ORIGIN_DEFAULT_QUOTA, totalUnusedQuota);
         } else {
@@ -225,21 +290,12 @@
 
     // 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();
+    private long getGlobalLimit() {
+        long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
+        long fileSystemSize = mDiskInfo.getTotalSizeBytes();
         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
@@ -270,13 +326,4 @@
     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/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java b/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
index 4ba758d..a2604e0 100644
--- a/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
+++ b/tests/src/com/android/browser/WebStorageSizeManagerUnitTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008 The Android Open Source Project
+ * 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.
@@ -18,6 +18,7 @@
 
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
+import android.webkit.WebStorage;
 
 /**
  * This is a series of unit tests for the WebStorageSizeManager class.
@@ -25,7 +26,138 @@
  */
 @MediumTest
 public class WebStorageSizeManagerUnitTests extends AndroidTestCase {
+    // Used for testing the out-of-space callbacks.
+    private long mNewQuota;
+    // Callback functor that sets a new quota in case of out-of-space scenarios.
+    private class MockQuotaUpdater implements WebStorage.QuotaUpdater {
+        public void updateQuota(long newQuota) {
+            mNewQuota = newQuota;
+        }
+    }
 
+    // Mock the DiskInfo.
+    private class MockDiskInfo implements WebStorageSizeManager.DiskInfo {
+        private long mFreeSize;
+        private long mTotalSize;
+
+        public long getFreeSpaceSizeBytes() {
+            return mFreeSize;
+        }
+
+        public long getTotalSizeBytes() {
+            return mTotalSize;
+        }
+
+        public void setFreeSpaceSizeBytes(long freeSize) {
+            mFreeSize = freeSize;
+        }
+
+        public void setTotalSizeBytes(long totalSize) {
+            mTotalSize = totalSize;
+        }
+    }
+
+    // Mock the AppCacheInfo
+    public class MockAppCacheInfo implements WebStorageSizeManager.AppCacheInfo {
+        private long mAppCacheSize;
+
+        public long getAppCacheSizeBytes() {
+            return mAppCacheSize;
+        }
+
+        public void setAppCacheSizeBytes(long appCacheSize) {
+            mAppCacheSize = appCacheSize;
+        }
+    }
+
+    private MockQuotaUpdater mQuotaUpdater = new MockQuotaUpdater();
+    private final MockDiskInfo mDiskInfo = new MockDiskInfo();
+    private final MockAppCacheInfo mAppCacheInfo = new MockAppCacheInfo();
+    // Utility for making size computations easier to read.
+    private long bytes(double megabytes) {
+        return (new Double(megabytes * 1024 * 1024)).longValue();
+    }
+    /**
+     * Test the onExceededDatabaseQuota and onReachedMaxAppCacheSize callbacks
+     */
+    public void testCallbacks() {
+        long totalUsedQuota = 0;
+        final long defaultQuota = WebStorageSizeManager.ORIGIN_DEFAULT_QUOTA;  // 3MB
+        final long quotaIncrease = WebStorageSizeManager.QUOTA_INCREASE_STEP;  // 1MB
+        // We have 75 MB total, 24MB free so the global limit will be 12 MB.
+        mDiskInfo.setTotalSizeBytes(bytes(75));
+        mDiskInfo.setFreeSpaceSizeBytes(bytes(24));
+        // We have an appcache file size of 0 MB.
+        mAppCacheInfo.setAppCacheSizeBytes(0);
+        // Create the manager.
+        WebStorageSizeManager manager = new WebStorageSizeManager(null, mDiskInfo, mAppCacheInfo);
+        // We add origin 1.
+        long origin1Quota = 0;
+        manager.onExceededDatabaseQuota("1", "1", origin1Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota, mNewQuota);
+        origin1Quota = mNewQuota;
+        totalUsedQuota += origin1Quota;
+
+        // We add origin 2.
+        long origin2Quota = 0;
+        manager.onExceededDatabaseQuota("2", "2", origin2Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota, mNewQuota);
+        origin2Quota = mNewQuota;
+        totalUsedQuota += origin2Quota;
+
+        // Origin 1 runs out of space.
+        manager.onExceededDatabaseQuota("1", "1", origin1Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota + quotaIncrease, mNewQuota);
+        totalUsedQuota -= origin1Quota;
+        origin1Quota = mNewQuota;
+        totalUsedQuota += origin1Quota;
+
+        // Origin 2 runs out of space.
+        manager.onExceededDatabaseQuota("2", "2", origin2Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(defaultQuota + quotaIncrease, mNewQuota);
+        totalUsedQuota -= origin2Quota;
+        origin2Quota = mNewQuota;
+        totalUsedQuota += origin2Quota;
+
+        // We add origin 3. TotalUsedQuota is 8 (3 + 3 + 1 + 1). AppCacheMaxSize is 3 (12 / 4).
+        // So we have 1 MB free.
+        long origin3Quota = 0;
+        manager.onExceededDatabaseQuota("3", "3", origin3Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(bytes(1), mNewQuota);  // 1MB
+        origin3Quota = mNewQuota;
+        totalUsedQuota += origin3Quota;
+
+        // Origin 1 runs out of space again. We're also out of space so we can't give it more.
+        manager.onExceededDatabaseQuota("1", "1", origin1Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(origin1Quota, mNewQuota);
+
+        // We try adding a new origin. Which will fail.
+        manager.onExceededDatabaseQuota("4", "4", 0, totalUsedQuota, mQuotaUpdater);
+        assertEquals(0, mNewQuota);
+
+        // AppCache size increases to 2MB...
+        mAppCacheInfo.setAppCacheSizeBytes(bytes(2));
+        // ... and wants 2MB more. Fail.
+        manager.onReachedMaxAppCacheSize(bytes(2), totalUsedQuota, mQuotaUpdater);
+        assertEquals(0, mNewQuota);
+
+        // The user nukes origin 2
+        totalUsedQuota -= origin2Quota;
+        origin2Quota = 0;
+        // TotalUsedQuota is 5 (9 - 4). AppCacheMaxSize is 3. AppCacheSize is 2.
+        // AppCache wants 1.5MB more
+        manager.onReachedMaxAppCacheSize(bytes(1.5), totalUsedQuota, mQuotaUpdater);
+        mAppCacheInfo.setAppCacheSizeBytes(mAppCacheInfo.getAppCacheSizeBytes() + bytes(2.5));
+        assertEquals(mAppCacheInfo.getAppCacheSizeBytes(), mNewQuota);
+
+        // We try adding a new origin. This time we succeed.
+        // TotalUsedQuota is 5. AppCacheMaxSize is 4.5. So we have 12 - 9.5 = 2.5 available.
+        long origin4Quota = 0;
+        manager.onExceededDatabaseQuota("4", "4", origin4Quota, totalUsedQuota, mQuotaUpdater);
+        assertEquals(bytes(2.5), mNewQuota);
+        origin4Quota = mNewQuota;
+        totalUsedQuota += origin4Quota;
+    }
     /**
      * Test the application caches max size calculator.
      */