2 * Copyright (C) 2009 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.browser;
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;
33 * Package level class for managing the disk size consumed by the WebDatabase
34 * and ApplicationCaches APIs (henceforth called Web storage).
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.
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.
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.
53 * The size of the disk space used for Web storage is initially divided between
54 * WebDatabase and ApplicationCaches as follows:
57 * 25% for ApplicationCaches
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.
66 * When the total ApplicationCaches usage reaches its current quota, WebKit
67 * invokes the following callback function:
68 * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
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.
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).
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
85 class WebStorageSizeManager {
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;
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.
120 public interface DiskInfo {
122 * @return the size of the free space in the file system.
124 public long getFreeSpaceSizeBytes();
127 * @return the total size of the file system.
129 public long getTotalSizeBytes();
132 private DiskInfo mDiskInfo;
133 // For convenience, we provide a DiskInfo implementation that uses StatFs.
134 public static class StatFsDiskInfo implements DiskInfo {
137 public StatFsDiskInfo(String path) {
138 mFs = new StatFs(path);
141 public long getFreeSpaceSizeBytes() {
142 return mFs.getAvailableBlocks() * mFs.getBlockSize();
145 public long getTotalSizeBytes() {
146 return mFs.getBlockCount() * mFs.getBlockSize();
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.
155 public interface AppCacheInfo {
157 * @return the current size of the appcache file.
159 public long getAppCacheSizeBytes();
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;
169 public WebKitAppCacheInfo(String path) {
170 mAppCachePath = path;
173 public long getAppCacheSizeBytes() {
174 File file = new File(mAppCachePath
177 return file.length();
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
188 public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
189 AppCacheInfo appCacheInfo) {
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());
200 * Returns the maximum size of the application cache.
202 public long getAppCacheMaxSize() {
203 return mAppCacheMaxSize;
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 totalUsedQuota is the sum of all origins' quota.
213 * @param quotaUpdater The callback to run when a decision to allow or
214 * deny quota has been made. Don't forget to call this!
216 public void onExceededDatabaseQuota(String url,
217 String databaseIdentifier, long currentQuota, long estimatedSize,
218 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
221 "Received onExceededDatabaseQuota for "
227 + ", total used quota: "
231 long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
233 if (totalUnusedQuota <= 0) {
234 // There definitely isn't any more space. Fire notifications
235 // if needed and exit.
236 if (totalUsedQuota > 0) {
237 // We only fire the notification if there are some other websites
238 // using some of the quota. This avoids the degenerate case where
239 // the first ever website to use Web storage tries to use more
240 // data than it is actually available. In such a case, showing
241 // the notification would not help at all since there is nothing
243 scheduleOutOfSpaceNotification();
245 quotaUpdater.updateQuota(currentQuota);
247 Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
251 // We have enough space inside mGlobalLimit.
252 long newOriginQuota = currentQuota;
253 if (newOriginQuota == 0) {
254 // This is a new origin, give it the size it asked for if possible.
255 // If we cannot satisfy the estimatedSize, we should return 0 as
256 // returning a value less that what the site requested will lead
257 // to webcore not creating the database.
258 if (totalUnusedQuota >= estimatedSize) {
259 newOriginQuota = estimatedSize;
263 "onExceededDatabaseQuota: Unable to satisfy" +
264 " estimatedSize for the new database " +
265 " (estimatedSize: " + estimatedSize +
266 ", unused quota: " + totalUnusedQuota);
271 // This is an origin we have seen before. It wants a quota
274 Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
276 quotaUpdater.updateQuota(newOriginQuota);
279 Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
285 * The Application Cache has exceeded its max size.
286 * @param spaceNeeded is the amount of disk space that would be needed
287 * in order for the last appcache operation to succeed.
288 * @param totalUsedQuota is the sum of all origins' quota.
289 * @param quotaUpdater A callback to inform the WebCore thread that a new
290 * app cache size is available. This callback must always be executed at
291 * some point to ensure that the sleeping WebCore thread is woken up.
293 public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
294 WebStorage.QuotaUpdater quotaUpdater) {
296 Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
297 + spaceNeeded + " bytes.");
300 long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
302 if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
303 // There definitely isn't any more space. Fire notifications
304 // if needed and exit.
305 if (totalUsedQuota > 0) {
306 // We only fire the notification if there are some other websites
307 // using some of the quota. This avoids the degenerate case where
308 // the first ever website to use Web storage tries to use more
309 // data than it is actually available. In such a case, showing
310 // the notification would not help at all since there is nothing
312 scheduleOutOfSpaceNotification();
314 quotaUpdater.updateQuota(0);
316 Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
320 // There is enough space to accommodate spaceNeeded bytes.
321 mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
322 quotaUpdater.updateQuota(mAppCacheMaxSize);
325 Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
330 // Reset the notification time; we use this iff the user
331 // use clear all; we reset it to some time in the future instead
332 // of just setting it to -1, as the clear all method is asynchronous
333 static void resetLastOutOfSpaceNotificationTime() {
334 mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
335 NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
338 // Computes the global limit as a function of the size of the data
339 // partition and the amount of free space on that partition.
340 private long getGlobalLimit() {
341 long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
342 long fileSystemSize = mDiskInfo.getTotalSizeBytes();
343 return calculateGlobalLimit(fileSystemSize, freeSpace);
346 /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
347 long freeSpaceBytes) {
348 if (fileSystemSizeBytes <= 0
349 || freeSpaceBytes <= 0
350 || freeSpaceBytes > fileSystemSizeBytes) {
354 long fileSystemSizeRatio =
355 2 << ((int) Math.floor(Math.log10(
356 fileSystemSizeBytes / (1024 * 1024))));
357 long maxSizeBytes = (long) Math.min(Math.floor(
358 fileSystemSizeBytes / fileSystemSizeRatio),
359 Math.floor(freeSpaceBytes / 2));
360 // Round maxSizeBytes up to a multiple of 1024KB (but only if
361 // maxSizeBytes > 1MB).
362 long maxSizeStepBytes = 1024 * 1024;
363 if (maxSizeBytes < maxSizeStepBytes) {
366 long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
367 return (maxSizeStepBytes
368 * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
371 // Schedules a system notification that takes the user to the WebSettings
372 // activity when clicked.
373 private void scheduleOutOfSpaceNotification() {
375 Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
377 if (mContext == null) {
378 // mContext can be null if we're running unit tests.
381 if ((mLastOutOfSpaceNotificationTime == -1) ||
382 (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
383 // setup the notification boilerplate.
384 int icon = android.R.drawable.stat_sys_warning;
385 CharSequence title = mContext.getString(
386 R.string.webstorage_outofspace_notification_title);
387 CharSequence text = mContext.getString(
388 R.string.webstorage_outofspace_notification_text);
389 long when = System.currentTimeMillis();
390 Intent intent = new Intent(mContext, WebsiteSettingsActivity.class);
391 PendingIntent contentIntent =
392 PendingIntent.getActivity(mContext, 0, intent, 0);
393 Notification notification = new Notification(icon, title, when);
394 notification.setLatestEventInfo(mContext, title, text, contentIntent);
395 notification.flags |= Notification.FLAG_AUTO_CANCEL;
397 String ns = Context.NOTIFICATION_SERVICE;
398 NotificationManager mgr =
399 (NotificationManager) mContext.getSystemService(ns);
401 mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
402 mgr.notify(OUT_OF_SPACE_ID, notification);