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