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 */ 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 (long)(mFs.getAvailableBlocks()) * mFs.getBlockSize(); 145 } 146 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 */ 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.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 */ 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 */ 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 */ 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 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. 358 private long getGlobalLimit() { 359 long freeSpace = mDiskInfo.getFreeSpaceSizeBytes(); 360 long fileSystemSize = mDiskInfo.getTotalSizeBytes(); 361 return calculateGlobalLimit(fileSystemSize, freeSpace); 362 } 363 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. 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