/* * 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 com.android.browser.preferences.WebsiteSettingsFragment; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.os.StatFs; import android.preference.PreferenceActivity; import android.util.Log; import android.webkit.WebStorage; import java.io.File; /** * 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 its estimated size. If we cannot satisfy * the estimated size, then WebCore will not create the database. * Quota increases are done in steps, where the increase step is * min(QUOTA_INCREASE_STEP, unused_quota). * * 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. */ public 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. public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024; // 3MB // The default value for quota increases. public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024; // 1MB // Extra padding space for appcache maximum size increases. This is needed // because WebKit sends us an estimate of the amount of space needed // but this estimate may, currently, be slightly less than what is actually // needed. We therefore add some 'padding'. // TODO(andreip): fix this in WebKit. public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB // The system status bar notification id. private final static int OUT_OF_SPACE_ID = 1; // The time of the last out of space notification private static long mLastOutOfSpaceNotificationTime = -1; // Delay between two notification in ms private final static long NOTIFICATION_INTERVAL = 5 * 60 * 1000; // Delay in ms used when resetting the notification time private final static long RESET_NOTIFICATION_INTERVAL = 3 * 1000; // The application context. private final Context mContext; // The global Web storage limit. private final long mGlobalLimit; // The maximum size of the application cache file. private long mAppCacheMaxSize; /** * 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 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 (long)(mFs.getAvailableBlocks()) * mFs.getBlockSize(); } public long getTotalSizeBytes() { return (long)(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.getApplicationContext(); 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()); } /** * 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 estimatedSize the estimated size of a new database, or 0 if * this has been invoked in response to an existing database * overflowing its quota. * @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 estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { if(LOGV_ENABLED) { Log.v(LOGTAG, "Received onExceededDatabaseQuota for " + url + ":" + databaseIdentifier + "(current quota: " + currentQuota + ", total used quota: " + totalUsedQuota + ")"); } long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize; if (totalUnusedQuota <= 0) { // There definitely isn't any more space. Fire notifications // if needed and exit. if (totalUsedQuota > 0) { // We only fire the notification if there are some other websites // using some of the quota. This avoids the degenerate case where // the first ever website to use Web storage tries to use more // data than it is actually available. In such a case, showing // the notification would not help at all since there is nothing // the user can do. scheduleOutOfSpaceNotification(); } quotaUpdater.updateQuota(currentQuota); if(LOGV_ENABLED) { Log.v(LOGTAG, "onExceededDatabaseQuota: out of space."); } return; } // We have some space inside mGlobalLimit. long newOriginQuota = currentQuota; if (newOriginQuota == 0) { // This is a new origin, give it the size it asked for if possible. // If we cannot satisfy the estimatedSize, we should return 0 as // returning a value less that what the site requested will lead // to webcore not creating the database. if (totalUnusedQuota >= estimatedSize) { newOriginQuota = estimatedSize; } else { if (LOGV_ENABLED) { Log.v(LOGTAG, "onExceededDatabaseQuota: Unable to satisfy" + " estimatedSize for the new database " + " (estimatedSize: " + estimatedSize + ", unused quota: " + totalUnusedQuota); } newOriginQuota = 0; } } else { // This is an origin we have seen before. It wants a quota // increase. There are two circumstances: either the origin // is creating a new database or it has overflowed an existing database. // Increase the quota. If estimatedSize == 0, then this is a quota overflow // rather than the creation of a new database. long quotaIncrease = estimatedSize == 0 ? Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota) : estimatedSize; newOriginQuota += quotaIncrease; if (quotaIncrease > totalUnusedQuota) { // We can't fit, so deny quota. newOriginQuota = currentQuota; } } 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 + APPCACHE_MAXSIZE_PADDING) { // There definitely isn't any more space. Fire notifications // if needed and exit. if (totalUsedQuota > 0) { // We only fire the notification if there are some other websites // using some of the quota. This avoids the degenerate case where // the first ever website to use Web storage tries to use more // data than it is actually available. In such a case, showing // the notification would not help at all since there is nothing // the user can do. 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 + APPCACHE_MAXSIZE_PADDING; quotaUpdater.updateQuota(mAppCacheMaxSize); if(LOGV_ENABLED) { Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to " + mAppCacheMaxSize); } } // Reset the notification time; we use this iff the user // use clear all; we reset it to some time in the future instead // of just setting it to -1, as the clear all method is asynchronous public static void resetLastOutOfSpaceNotificationTime() { mLastOutOfSpaceNotificationTime = System.currentTimeMillis() - NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL; } // 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() { long freeSpace = mDiskInfo.getFreeSpaceSizeBytes(); long fileSystemSize = mDiskInfo.getTotalSizeBytes(); return calculateGlobalLimit(fileSystemSize, freeSpace); } /*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() { if(LOGV_ENABLED) { Log.v(LOGTAG, "scheduleOutOfSpaceNotification called."); } if ((mLastOutOfSpaceNotificationTime == -1) || (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) { // setup the notification boilerplate. int icon = android.R.drawable.stat_sys_warning; CharSequence title = mContext.getString( R.string.webstorage_outofspace_notification_title); CharSequence text = mContext.getString( R.string.webstorage_outofspace_notification_text); long when = System.currentTimeMillis(); Intent intent = new Intent(mContext, BrowserPreferencesPage.class); intent.putExtra(PreferenceActivity.EXTRA_SHOW_FRAGMENT, WebsiteSettingsFragment.class.getName()); PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0); Notification notification = new Notification(icon, title, when); notification.setLatestEventInfo(mContext, title, text, contentIntent); notification.flags |= Notification.FLAG_AUTO_CANCEL; // Fire away. String ns = Context.NOTIFICATION_SERVICE; NotificationManager mgr = (NotificationManager) mContext.getSystemService(ns); if (mgr != null) { mLastOutOfSpaceNotificationTime = System.currentTimeMillis(); mgr.notify(OUT_OF_SPACE_ID, notification); } } } }