| /* |
| * 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); |
| } |
| } |
| } |
| } |