PageRenderTime 76ms CodeModel.GetById 49ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/WebVox/src/com/marvin/webvox/WebStorageSizeManager.java

http://eyes-free.googlecode.com/
Java | 408 lines | 205 code | 30 blank | 173 comment | 28 complexity | df762336bf679be21ce01b65ae2b8f05 MD5 | raw file
  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
 17package com.marvin.webvox;
 18
 19import com.marvin.webvox.R;
 20
 21import android.app.Notification;
 22import android.app.NotificationManager;
 23import android.app.PendingIntent;
 24import android.content.Context;
 25import android.content.Intent;
 26import android.os.StatFs;
 27import android.util.Log;
 28import android.webkit.WebStorage;
 29
 30import java.io.File;
 31import java.util.Set;
 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 */
 87class WebStorageSizeManager {
 88    // Logging flags.
 89    private final static boolean LOGV_ENABLED = com.marvin.webvox.Browser.LOGV_ENABLED;
 90    private final static boolean LOGD_ENABLED = com.marvin.webvox.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         */
126        public long getFreeSpaceSizeBytes();
127
128        /**
129         * @return the total size of the file system.
130         */
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
139        public StatFsDiskInfo(String path) {
140            mFs = new StatFs(path);
141        }
142
143        public long getFreeSpaceSizeBytes() {
144            return mFs.getAvailableBlocks() * mFs.getBlockSize();
145        }
146
147        public long getTotalSizeBytes() {
148            return 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         */
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
171        public WebKitAppCacheInfo(String path) {
172            mAppCachePath = path;
173        }
174
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     */
190    public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
191            AppCacheInfo appCacheInfo) {
192        mContext = ctx;
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     */
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 totalUsedQuota is the sum of all origins' quota.
215     * @param quotaUpdater The callback to run when a decision to allow or
216     *     deny quota has been made. Don't forget to call this!
217     */
218    public void onExceededDatabaseQuota(String url,
219        String databaseIdentifier, long currentQuota, long estimatedSize,
220        long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
221        if(LOGV_ENABLED) {
222            Log.v(LOGTAG,
223                  "Received onExceededDatabaseQuota for "
224                  + url
225                  + ":"
226                  + databaseIdentifier
227                  + "(current quota: "
228                  + currentQuota
229                  + ", total used quota: "
230                  + totalUsedQuota
231                  + ")");
232        }
233        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
234
235        if (totalUnusedQuota <= 0) {
236            // There definitely isn't any more space. Fire notifications
237            // if needed and exit.
238            if (totalUsedQuota > 0) {
239                // We only fire the notification if there are some other websites
240                // using some of the quota. This avoids the degenerate case where
241                // the first ever website to use Web storage tries to use more
242                // data than it is actually available. In such a case, showing
243                // the notification would not help at all since there is nothing
244                // the user can do.
245                scheduleOutOfSpaceNotification();
246            }
247            quotaUpdater.updateQuota(currentQuota);
248            if(LOGV_ENABLED) {
249                Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
250            }
251            return;
252        }
253        // We have enough space inside mGlobalLimit.
254        long newOriginQuota = currentQuota;
255        if (newOriginQuota == 0) {
256            // This is a new origin, give it the size it asked for if possible.
257            // If we cannot satisfy the estimatedSize, we should return 0 as
258            // returning a value less that what the site requested will lead
259            // to webcore not creating the database.
260            if (totalUnusedQuota >= estimatedSize) {
261                newOriginQuota = estimatedSize;
262            } else {
263                if (LOGV_ENABLED) {
264                    Log.v(LOGTAG,
265                          "onExceededDatabaseQuota: Unable to satisfy" +
266                          " estimatedSize for the new database " +
267                          " (estimatedSize: " + estimatedSize +
268                          ", unused quota: " + totalUnusedQuota);
269                }
270                newOriginQuota = 0;
271            }
272        } else {
273            // This is an origin we have seen before. It wants a quota
274            // increase.
275            newOriginQuota +=
276                Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
277        }
278        quotaUpdater.updateQuota(newOriginQuota);
279
280        if(LOGV_ENABLED) {
281            Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
282                    + newOriginQuota);
283        }
284    }
285
286    /**
287     * The Application Cache has exceeded its max size.
288     * @param spaceNeeded is the amount of disk space that would be needed
289     * in order for the last appcache operation to succeed.
290     * @param totalUsedQuota is the sum of all origins' quota.
291     * @param quotaUpdater A callback to inform the WebCore thread that a new
292     * app cache size is available. This callback must always be executed at
293     * some point to ensure that the sleeping WebCore thread is woken up.
294     */
295    public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
296            WebStorage.QuotaUpdater quotaUpdater) {
297        if(LOGV_ENABLED) {
298            Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
299                  + spaceNeeded + " bytes.");
300        }
301
302        long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
303
304        if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
305            // There definitely isn't any more space. Fire notifications
306            // if needed and exit.
307            if (totalUsedQuota > 0) {
308                // We only fire the notification if there are some other websites
309                // using some of the quota. This avoids the degenerate case where
310                // the first ever website to use Web storage tries to use more
311                // data than it is actually available. In such a case, showing
312                // the notification would not help at all since there is nothing
313                // the user can do.
314                scheduleOutOfSpaceNotification();
315            }
316            quotaUpdater.updateQuota(0);
317            if(LOGV_ENABLED) {
318                Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
319            }
320            return;
321        }
322        // There is enough space to accommodate spaceNeeded bytes.
323        mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
324        quotaUpdater.updateQuota(mAppCacheMaxSize);
325
326        if(LOGV_ENABLED) {
327            Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
328                    + mAppCacheMaxSize);
329        }
330    }
331
332    // Reset the notification time; we use this iff the user
333    // use clear all; we reset it to some time in the future instead
334    // of just setting it to -1, as the clear all method is asynchronous
335    static void resetLastOutOfSpaceNotificationTime() {
336        mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
337            NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
338    }
339
340    // Computes the global limit as a function of the size of the data
341    // partition and the amount of free space on that partition.
342    private long getGlobalLimit() {
343        long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
344        long fileSystemSize = mDiskInfo.getTotalSizeBytes();
345        return calculateGlobalLimit(fileSystemSize, freeSpace);
346    }
347
348    /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
349            long freeSpaceBytes) {
350        if (fileSystemSizeBytes <= 0
351                || freeSpaceBytes <= 0
352                || freeSpaceBytes > fileSystemSizeBytes) {
353            return 0;
354        }
355
356        long fileSystemSizeRatio =
357            2 << ((int) Math.floor(Math.log10(
358                    fileSystemSizeBytes / (1024 * 1024))));
359        long maxSizeBytes = (long) Math.min(Math.floor(
360                fileSystemSizeBytes / fileSystemSizeRatio),
361                Math.floor(freeSpaceBytes / 2));
362        // Round maxSizeBytes up to a multiple of 1024KB (but only if
363        // maxSizeBytes > 1MB).
364        long maxSizeStepBytes = 1024 * 1024;
365        if (maxSizeBytes < maxSizeStepBytes) {
366            return 0;
367        }
368        long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
369        return (maxSizeStepBytes
370                * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
371    }
372
373    // Schedules a system notification that takes the user to the WebSettings
374    // activity when clicked.
375    private void scheduleOutOfSpaceNotification() {
376        if(LOGV_ENABLED) {
377            Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
378        }
379        if (mContext == null) {
380            // mContext can be null if we're running unit tests.
381            return;
382        }
383        if ((mLastOutOfSpaceNotificationTime == -1) ||
384            (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
385            // setup the notification boilerplate.
386            int icon = android.R.drawable.stat_sys_warning;
387            CharSequence title = mContext.getString(
388                    R.string.webstorage_outofspace_notification_title);
389            CharSequence text = mContext.getString(
390                    R.string.webstorage_outofspace_notification_text);
391            long when = System.currentTimeMillis();
392            Intent intent = new Intent(mContext, WebsiteSettingsActivity.class);
393            PendingIntent contentIntent =
394                PendingIntent.getActivity(mContext, 0, intent, 0);
395            Notification notification = new Notification(icon, title, when);
396            notification.setLatestEventInfo(mContext, title, text, contentIntent);
397            notification.flags |= Notification.FLAG_AUTO_CANCEL;
398            // Fire away.
399            String ns = Context.NOTIFICATION_SERVICE;
400            NotificationManager mgr =
401                (NotificationManager) mContext.getSystemService(ns);
402            if (mgr != null) {
403                mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
404                mgr.notify(OUT_OF_SPACE_ID, notification);
405            }
406        }
407    }
408}