• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
getFreeSpaceSizeBytes()126         public long getFreeSpaceSizeBytes();
127 
128         /**
129          * @return the total size of the file system.
130          */
getTotalSizeBytes()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 
StatFsDiskInfo(String path)139         public StatFsDiskInfo(String path) {
140             mFs = new StatFs(path);
141         }
142 
getFreeSpaceSizeBytes()143         public long getFreeSpaceSizeBytes() {
144             return (long)(mFs.getAvailableBlocks()) * mFs.getBlockSize();
145         }
146 
getTotalSizeBytes()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          */
getAppCacheSizeBytes()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 
WebKitAppCacheInfo(String path)171         public WebKitAppCacheInfo(String path) {
172             mAppCachePath = path;
173         }
174 
getAppCacheSizeBytes()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      */
WebStorageSizeManager(Context ctx, DiskInfo diskInfo, AppCacheInfo appCacheInfo)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      */
getAppCacheMaxSize()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      */
onExceededDatabaseQuota(String url, String databaseIdentifier, long currentQuota, long estimatedSize, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)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      */
onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater)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
resetLastOutOfSpaceNotificationTime()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.
getGlobalLimit()358     private long getGlobalLimit() {
359         long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
360         long fileSystemSize = mDiskInfo.getTotalSizeBytes();
361         return calculateGlobalLimit(fileSystemSize, freeSpace);
362     }
363 
calculateGlobalLimit(long fileSystemSizeBytes, long freeSpaceBytes)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.
scheduleOutOfSpaceNotification()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