OSDN Git Service

Deprecate fill_parent and introduce match_parent.
[android-x86/packages-apps-Browser.git] / src / com / android / browser / WebStorageSizeManager.java
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 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;
27
28 import java.io.File;
29 import java.util.Set;
30
31
32 /**
33  * Package level class for managing the disk size consumed by the WebDatabase
34  * and ApplicationCaches APIs (henceforth called Web storage).
35  *
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.
40  *
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.
45  *
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.
52  *
53  * The size of the disk space used for Web storage is initially divided between
54  * WebDatabase and ApplicationCaches as follows:
55  *
56  * 75% for WebDatabase
57  * 25% for ApplicationCaches
58  *
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.
65  *
66  * When the total ApplicationCaches usage reaches its current quota, WebKit
67  * invokes the following callback function:
68  * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
69  *
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.
74  *
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).
79  *
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
83  * by an origin.
84  */
85 class WebStorageSizeManager {
86     // Logging flags.
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;
114
115     /**
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.
119      */
120     public interface DiskInfo {
121         /**
122          * @return the size of the free space in the file system.
123          */
124         public long getFreeSpaceSizeBytes();
125
126         /**
127          * @return the total size of the file system.
128          */
129         public long getTotalSizeBytes();
130     };
131
132     private DiskInfo mDiskInfo;
133     // For convenience, we provide a DiskInfo implementation that uses StatFs.
134     public static class StatFsDiskInfo implements DiskInfo {
135         private StatFs mFs;
136
137         public StatFsDiskInfo(String path) {
138             mFs = new StatFs(path);
139         }
140
141         public long getFreeSpaceSizeBytes() {
142             return mFs.getAvailableBlocks() * mFs.getBlockSize();
143         }
144
145         public long getTotalSizeBytes() {
146             return mFs.getBlockCount() * mFs.getBlockSize();
147         }
148     };
149
150     /**
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.
154      */
155     public interface AppCacheInfo {
156         /**
157          * @return the current size of the appcache file.
158          */
159         public long getAppCacheSizeBytes();
160     };
161
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;
168
169         public WebKitAppCacheInfo(String path) {
170             mAppCachePath = path;
171         }
172
173         public long getAppCacheSizeBytes() {
174             File file = new File(mAppCachePath
175                     + File.separator
176                     + APPCACHE_FILE);
177             return file.length();
178         }
179     };
180
181     /**
182      * Public ctor
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
186      * appcache file.
187      */
188     public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
189             AppCacheInfo appCacheInfo) {
190         mContext = ctx;
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());
197     }
198
199     /**
200      * Returns the maximum size of the application cache.
201      */
202     public long getAppCacheMaxSize() {
203         return mAppCacheMaxSize;
204     }
205
206     /**
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!
215      */
216     public void onExceededDatabaseQuota(String url,
217         String databaseIdentifier, long currentQuota, long estimatedSize,
218         long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
219         if(LOGV_ENABLED) {
220             Log.v(LOGTAG,
221                   "Received onExceededDatabaseQuota for "
222                   + url
223                   + ":"
224                   + databaseIdentifier
225                   + "(current quota: "
226                   + currentQuota
227                   + ", total used quota: "
228                   + totalUsedQuota
229                   + ")");
230         }
231         long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
232
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
242                 // the user can do.
243                 scheduleOutOfSpaceNotification();
244             }
245             quotaUpdater.updateQuota(currentQuota);
246             if(LOGV_ENABLED) {
247                 Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
248             }
249             return;
250         }
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;
260             } else {
261                 if (LOGV_ENABLED) {
262                     Log.v(LOGTAG,
263                           "onExceededDatabaseQuota: Unable to satisfy" +
264                           " estimatedSize for the new database " +
265                           " (estimatedSize: " + estimatedSize +
266                           ", unused quota: " + totalUnusedQuota);
267                 }
268                 newOriginQuota = 0;
269             }
270         } else {
271             // This is an origin we have seen before. It wants a quota
272             // increase.
273             newOriginQuota +=
274                 Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
275         }
276         quotaUpdater.updateQuota(newOriginQuota);
277
278         if(LOGV_ENABLED) {
279             Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
280                     + newOriginQuota);
281         }
282     }
283
284     /**
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.
292      */
293     public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
294             WebStorage.QuotaUpdater quotaUpdater) {
295         if(LOGV_ENABLED) {
296             Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
297                   + spaceNeeded + " bytes.");
298         }
299
300         long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
301
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
311                 // the user can do.
312                 scheduleOutOfSpaceNotification();
313             }
314             quotaUpdater.updateQuota(0);
315             if(LOGV_ENABLED) {
316                 Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
317             }
318             return;
319         }
320         // There is enough space to accommodate spaceNeeded bytes.
321         mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
322         quotaUpdater.updateQuota(mAppCacheMaxSize);
323
324         if(LOGV_ENABLED) {
325             Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
326                     + mAppCacheMaxSize);
327         }
328     }
329
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;
336     }
337
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);
344     }
345
346     /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
347             long freeSpaceBytes) {
348         if (fileSystemSizeBytes <= 0
349                 || freeSpaceBytes <= 0
350                 || freeSpaceBytes > fileSystemSizeBytes) {
351             return 0;
352         }
353
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) {
364             return 0;
365         }
366         long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
367         return (maxSizeStepBytes
368                 * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
369     }
370
371     // Schedules a system notification that takes the user to the WebSettings
372     // activity when clicked.
373     private void scheduleOutOfSpaceNotification() {
374         if(LOGV_ENABLED) {
375             Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
376         }
377         if (mContext == null) {
378             // mContext can be null if we're running unit tests.
379             return;
380         }
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;
396             // Fire away.
397             String ns = Context.NOTIFICATION_SERVICE;
398             NotificationManager mgr =
399                 (NotificationManager) mContext.getSystemService(ns);
400             if (mgr != null) {
401                 mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
402                 mgr.notify(OUT_OF_SPACE_ID, notification);
403             }
404         }
405     }
406 }