Home | History | Annotate | Download | only in browser
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.browser;
     18 
     19 import android.app.Notification;
     20 import android.app.NotificationManager;
     21 import android.app.PendingIntent;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.os.StatFs;
     25 import android.util.Log;
     26 import android.webkit.WebStorage;
     27 
     28 import java.io.File;
     29 import java.util.Set;
     30 
     31 
     32 /**
     33  * Package level class for managing the disk size consumed by the WebDatabase
     34  * and ApplicationCaches APIs (henceforth called Web storage).
     35  *
     36  * Currently, the situation on the WebKit side is as follows:
     37  *  - WebDatabase enforces a quota for each origin.
     38  *  - Session/LocalStorage do not enforce any disk limits.
     39  *  - ApplicationCaches enforces a maximum size for all origins.
     40  *
     41  * The WebStorageSizeManager maintains a global limit for the disk space
     42  * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will
     43  * have a limit for Session/LocalStorage, this class will manage the space used
     44  * by those APIs as well.
     45  *
     46  * The global limit is computed as a function of the size of the partition where
     47  * these APIs store their data (they must store it on the same partition for
     48  * this to work) and the size of the available space on that partition.
     49  * The global limit is not subject to user configuration but we do provide
     50  * a debug-only setting.
     51  * TODO(andreip): implement the debug setting.
     52  *
     53  * The size of the disk space used for Web storage is initially divided between
     54  * WebDatabase and ApplicationCaches as follows:
     55  *
     56  * 75% for WebDatabase
     57  * 25% for ApplicationCaches
     58  *
     59  * When an origin's database usage reaches its current quota, WebKit invokes
     60  * the following callback function:
     61  * - exceededDatabaseQuota(Frame* frame, const String& database_name);
     62  * Note that the default quota for a new origin is 0, so we will receive the
     63  * 'exceededDatabaseQuota' callback before a new origin gets the chance to
     64  * create its first database.
     65  *
     66  * When the total ApplicationCaches usage reaches its current quota, WebKit
     67  * invokes the following callback function:
     68  * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
     69  *
     70  * The WebStorageSizeManager's main job is to respond to the above two callbacks
     71  * by inspecting the amount of unused Web storage quota (i.e. global limit -
     72  * sum of all other origins' quota) and deciding if a quota increase for the
     73  * out-of-space origin is allowed or not.
     74  *
     75  * The default quota for an origin is its estimated size. If we cannot satisfy
     76  * the estimated size, then WebCore will not create the database.
     77  * Quota increases are done in steps, where the increase step is
     78  * min(QUOTA_INCREASE_STEP, unused_quota).
     79  *
     80  * When all the Web storage space is used, the WebStorageSizeManager creates
     81  * a system notification that will guide the user to the WebSettings UI. There,
     82  * the user can free some of the Web storage space by deleting all the data used
     83  * by an origin.
     84  */
     85 class WebStorageSizeManager {
     86     // Logging flags.
     87     private final static boolean LOGV_ENABLED = com.android.browser.Browser.LOGV_ENABLED;
     88     private final static boolean LOGD_ENABLED = com.android.browser.Browser.LOGD_ENABLED;
     89     private final static String LOGTAG = "browser";
     90     // The default quota value for an origin.
     91     public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024;  // 3MB
     92     // The default value for quota increases.
     93     public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024;  // 1MB
     94     // Extra padding space for appcache maximum size increases. This is needed
     95     // because WebKit sends us an estimate of the amount of space needed
     96     // but this estimate may, currently, be slightly less than what is actually
     97     // needed. We therefore add some 'padding'.
     98     // TODO(andreip): fix this in WebKit.
     99     public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB
    100     // The system status bar notification id.
    101     private final static int OUT_OF_SPACE_ID = 1;
    102     // The time of the last out of space notification
    103     private static long mLastOutOfSpaceNotificationTime = -1;
    104     // Delay between two notification in ms
    105     private final static long NOTIFICATION_INTERVAL = 5 * 60 * 1000;
    106     // Delay in ms used when resetting the notification time
    107     private final static long RESET_NOTIFICATION_INTERVAL = 3 * 1000;
    108     // The application context.
    109     private final Context mContext;
    110     // The global Web storage limit.
    111     private final long mGlobalLimit;
    112     // The maximum size of the application cache file.
    113     private long mAppCacheMaxSize;
    114 
    115     /**
    116      * Interface used by the WebStorageSizeManager to obtain information
    117      * about the underlying file system. This functionality is separated
    118      * into its own interface mainly for testing purposes.
    119      */
    120     public interface DiskInfo {
    121         /**
    122          * @return the size of the free space in the file system.
    123          */
    124         public long getFreeSpaceSizeBytes();
    125 
    126         /**
    127          * @return the total size of the file system.
    128          */
    129         public long getTotalSizeBytes();
    130     };
    131 
    132     private DiskInfo mDiskInfo;
    133     // For convenience, we provide a DiskInfo implementation that uses StatFs.
    134     public static class StatFsDiskInfo implements DiskInfo {
    135         private StatFs mFs;
    136 
    137         public StatFsDiskInfo(String path) {
    138             mFs = new StatFs(path);
    139         }
    140 
    141         public long getFreeSpaceSizeBytes() {
    142             return mFs.getAvailableBlocks() * mFs.getBlockSize();
    143         }
    144 
    145         public long getTotalSizeBytes() {
    146             return mFs.getBlockCount() * mFs.getBlockSize();
    147         }
    148     };
    149 
    150     /**
    151      * Interface used by the WebStorageSizeManager to obtain information
    152      * about the appcache file. This functionality is separated into its own
    153      * interface mainly for testing purposes.
    154      */
    155     public interface AppCacheInfo {
    156         /**
    157          * @return the current size of the appcache file.
    158          */
    159         public long getAppCacheSizeBytes();
    160     };
    161 
    162     // For convenience, we provide an AppCacheInfo implementation.
    163     public static class WebKitAppCacheInfo implements AppCacheInfo {
    164         // The name of the application cache file. Keep in sync with
    165         // WebCore/loader/appcache/ApplicationCacheStorage.cpp
    166         private final static String APPCACHE_FILE = "ApplicationCache.db";
    167         private String mAppCachePath;
    168 
    169         public WebKitAppCacheInfo(String path) {
    170             mAppCachePath = path;
    171         }
    172 
    173         public long getAppCacheSizeBytes() {
    174             File file = new File(mAppCachePath
    175                     + File.separator
    176                     + APPCACHE_FILE);
    177             return file.length();
    178         }
    179     };
    180 
    181     /**
    182      * Public ctor
    183      * @param ctx is the application context
    184      * @param diskInfo is the DiskInfo instance used to query the file system.
    185      * @param appCacheInfo is the AppCacheInfo used to query info about the
    186      * appcache file.
    187      */
    188     public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
    189             AppCacheInfo appCacheInfo) {
    190         mContext = ctx;
    191         mDiskInfo = diskInfo;
    192         mGlobalLimit = getGlobalLimit();
    193         // The initial max size of the app cache is either 25% of the global
    194         // limit or the current size of the app cache file, whichever is bigger.
    195         mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
    196                 appCacheInfo.getAppCacheSizeBytes());
    197     }
    198 
    199     /**
    200      * Returns the maximum size of the application cache.
    201      */
    202     public long getAppCacheMaxSize() {
    203         return mAppCacheMaxSize;
    204     }
    205 
    206     /**
    207      * The origin has exceeded its database quota.
    208      * @param url the URL that exceeded the quota
    209      * @param databaseIdentifier the identifier of the database on
    210      *     which the transaction that caused the quota overflow was run
    211      * @param currentQuota the current quota for the origin.
    212      * @param estimatedSize the estimated size of a new database, or 0 if
    213      *     this has been invoked in response to an existing database
    214      *     overflowing its quota.
    215      * @param totalUsedQuota is the sum of all origins' quota.
    216      * @param quotaUpdater The callback to run when a decision to allow or
    217      *     deny quota has been made. Don't forget to call this!
    218      */
    219     public void onExceededDatabaseQuota(String url,
    220         String databaseIdentifier, long currentQuota, long estimatedSize,
    221         long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
    222         if(LOGV_ENABLED) {
    223             Log.v(LOGTAG,
    224                   "Received onExceededDatabaseQuota for "
    225                   + url
    226                   + ":"
    227                   + databaseIdentifier
    228                   + "(current quota: "
    229                   + currentQuota
    230                   + ", total used quota: "
    231                   + totalUsedQuota
    232                   + ")");
    233         }
    234         long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
    235 
    236         if (totalUnusedQuota <= 0) {
    237             // There definitely isn't any more space. Fire notifications
    238             // if needed and exit.
    239             if (totalUsedQuota > 0) {
    240                 // We only fire the notification if there are some other websites
    241                 // using some of the quota. This avoids the degenerate case where
    242                 // the first ever website to use Web storage tries to use more
    243                 // data than it is actually available. In such a case, showing
    244                 // the notification would not help at all since there is nothing
    245                 // the user can do.
    246                 scheduleOutOfSpaceNotification();
    247             }
    248             quotaUpdater.updateQuota(currentQuota);
    249             if(LOGV_ENABLED) {
    250                 Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
    251             }
    252             return;
    253         }
    254 
    255         // We have some space inside mGlobalLimit.
    256         long newOriginQuota = currentQuota;
    257         if (newOriginQuota == 0) {
    258             // This is a new origin, give it the size it asked for if possible.
    259             // If we cannot satisfy the estimatedSize, we should return 0 as
    260             // returning a value less that what the site requested will lead
    261             // to webcore not creating the database.
    262             if (totalUnusedQuota >= estimatedSize) {
    263                 newOriginQuota = estimatedSize;
    264             } else {
    265                 if (LOGV_ENABLED) {
    266                     Log.v(LOGTAG,
    267                             "onExceededDatabaseQuota: Unable to satisfy" +
    268                             " estimatedSize for the new database " +
    269                             " (estimatedSize: " + estimatedSize +
    270                             ", unused quota: " + totalUnusedQuota);
    271                 }
    272                 newOriginQuota = 0;
    273             }
    274         } else {
    275             // This is an origin we have seen before. It wants a quota
    276             // increase. There are two circumstances: either the origin
    277             // is creating a new database or it has overflowed an existing database.
    278 
    279             // Increase the quota. If estimatedSize == 0, then this is a quota overflow
    280             // rather than the creation of a new database.
    281             long quotaIncrease = estimatedSize == 0 ?
    282                     Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota) :
    283                     estimatedSize;
    284             newOriginQuota += quotaIncrease;
    285 
    286             if (quotaIncrease > totalUnusedQuota) {
    287                 // We can't fit, so deny quota.
    288                 newOriginQuota = currentQuota;
    289             }
    290         }
    291 
    292         quotaUpdater.updateQuota(newOriginQuota);
    293 
    294         if(LOGV_ENABLED) {
    295             Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
    296                     + newOriginQuota);
    297         }
    298     }
    299 
    300     /**
    301      * The Application Cache has exceeded its max size.
    302      * @param spaceNeeded is the amount of disk space that would be needed
    303      * in order for the last appcache operation to succeed.
    304      * @param totalUsedQuota is the sum of all origins' quota.
    305      * @param quotaUpdater A callback to inform the WebCore thread that a new
    306      * app cache size is available. This callback must always be executed at
    307      * some point to ensure that the sleeping WebCore thread is woken up.
    308      */
    309     public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
    310             WebStorage.QuotaUpdater quotaUpdater) {
    311         if(LOGV_ENABLED) {
    312             Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
    313                   + spaceNeeded + " bytes.");
    314         }
    315 
    316         long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
    317 
    318         if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
    319             // There definitely isn't any more space. Fire notifications
    320             // if needed and exit.
    321             if (totalUsedQuota > 0) {
    322                 // We only fire the notification if there are some other websites
    323                 // using some of the quota. This avoids the degenerate case where
    324                 // the first ever website to use Web storage tries to use more
    325                 // data than it is actually available. In such a case, showing
    326                 // the notification would not help at all since there is nothing
    327                 // the user can do.
    328                 scheduleOutOfSpaceNotification();
    329             }
    330             quotaUpdater.updateQuota(0);
    331             if(LOGV_ENABLED) {
    332                 Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
    333             }
    334             return;
    335         }
    336         // There is enough space to accommodate spaceNeeded bytes.
    337         mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
    338         quotaUpdater.updateQuota(mAppCacheMaxSize);
    339 
    340         if(LOGV_ENABLED) {
    341             Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
    342                     + mAppCacheMaxSize);
    343         }
    344     }
    345 
    346     // Reset the notification time; we use this iff the user
    347     // use clear all; we reset it to some time in the future instead
    348     // of just setting it to -1, as the clear all method is asynchronous
    349     static void resetLastOutOfSpaceNotificationTime() {
    350         mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
    351             NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
    352     }
    353 
    354     // Computes the global limit as a function of the size of the data
    355     // partition and the amount of free space on that partition.
    356     private long getGlobalLimit() {
    357         long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
    358         long fileSystemSize = mDiskInfo.getTotalSizeBytes();
    359         return calculateGlobalLimit(fileSystemSize, freeSpace);
    360     }
    361 
    362     /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
    363             long freeSpaceBytes) {
    364         if (fileSystemSizeBytes <= 0
    365                 || freeSpaceBytes <= 0
    366                 || freeSpaceBytes > fileSystemSizeBytes) {
    367             return 0;
    368         }
    369 
    370         long fileSystemSizeRatio =
    371             2 << ((int) Math.floor(Math.log10(
    372                     fileSystemSizeBytes / (1024 * 1024))));
    373         long maxSizeBytes = (long) Math.min(Math.floor(
    374                 fileSystemSizeBytes / fileSystemSizeRatio),
    375                 Math.floor(freeSpaceBytes / 2));
    376         // Round maxSizeBytes up to a multiple of 1024KB (but only if
    377         // maxSizeBytes > 1MB).
    378         long maxSizeStepBytes = 1024 * 1024;
    379         if (maxSizeBytes < maxSizeStepBytes) {
    380             return 0;
    381         }
    382         long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
    383         return (maxSizeStepBytes
    384                 * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
    385     }
    386 
    387     // Schedules a system notification that takes the user to the WebSettings
    388     // activity when clicked.
    389     private void scheduleOutOfSpaceNotification() {
    390         if(LOGV_ENABLED) {
    391             Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
    392         }
    393         if (mContext == null) {
    394             // mContext can be null if we're running unit tests.
    395             return;
    396         }
    397         if ((mLastOutOfSpaceNotificationTime == -1) ||
    398             (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
    399             // setup the notification boilerplate.
    400             int icon = android.R.drawable.stat_sys_warning;
    401             CharSequence title = mContext.getString(
    402                     R.string.webstorage_outofspace_notification_title);
    403             CharSequence text = mContext.getString(
    404                     R.string.webstorage_outofspace_notification_text);
    405             long when = System.currentTimeMillis();
    406             Intent intent = new Intent(mContext, WebsiteSettingsActivity.class);
    407             PendingIntent contentIntent =
    408                 PendingIntent.getActivity(mContext, 0, intent, 0);
    409             Notification notification = new Notification(icon, title, when);
    410             notification.setLatestEventInfo(mContext, title, text, contentIntent);
    411             notification.flags |= Notification.FLAG_AUTO_CANCEL;
    412             // Fire away.
    413             String ns = Context.NOTIFICATION_SERVICE;
    414             NotificationManager mgr =
    415                 (NotificationManager) mContext.getSystemService(ns);
    416             if (mgr != null) {
    417                 mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
    418                 mgr.notify(OUT_OF_SPACE_ID, notification);
    419             }
    420         }
    421     }
    422 }
    423