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 estimatedSize the estimated size of a new database, or 0 if 213 * this has been invoked in response to an existing database 214 * overflowing its quota. 215 * @param totalUsedQuota is the sum of all origins' quota. 216 * @param quotaUpdater The callback to run when a decision to allow or 217 * deny quota has been made. Don't forget to call this! 218 */ 219 public void onExceededDatabaseQuota(String url, 220 String databaseIdentifier, long currentQuota, long estimatedSize, 221 long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) { 222 if(LOGV_ENABLED) { 223 Log.v(LOGTAG, 224 "Received onExceededDatabaseQuota for " 225 + url 226 + ":" 227 + databaseIdentifier 228 + "(current quota: " 229 + currentQuota 230 + ", total used quota: " 231 + totalUsedQuota 232 + ")"); 233 } 234 long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize; 235 236 if (totalUnusedQuota <= 0) { 237 // There definitely isn't any more space. Fire notifications 238 // if needed and exit. 239 if (totalUsedQuota > 0) { 240 // We only fire the notification if there are some other websites 241 // using some of the quota. This avoids the degenerate case where 242 // the first ever website to use Web storage tries to use more 243 // data than it is actually available. In such a case, showing 244 // the notification would not help at all since there is nothing 245 // the user can do. 246 scheduleOutOfSpaceNotification(); 247 } 248 quotaUpdater.updateQuota(currentQuota); 249 if(LOGV_ENABLED) { 250 Log.v(LOGTAG, "onExceededDatabaseQuota: out of space."); 251 } 252 return; 253 } 254 255 // We have some space inside mGlobalLimit. 256 long newOriginQuota = currentQuota; 257 if (newOriginQuota == 0) { 258 // This is a new origin, give it the size it asked for if possible. 259 // If we cannot satisfy the estimatedSize, we should return 0 as 260 // returning a value less that what the site requested will lead 261 // to webcore not creating the database. 262 if (totalUnusedQuota >= estimatedSize) { 263 newOriginQuota = estimatedSize; 264 } else { 265 if (LOGV_ENABLED) { 266 Log.v(LOGTAG, 267 "onExceededDatabaseQuota: Unable to satisfy" + 268 " estimatedSize for the new database " + 269 " (estimatedSize: " + estimatedSize + 270 ", unused quota: " + totalUnusedQuota); 271 } 272 newOriginQuota = 0; 273 } 274 } else { 275 // This is an origin we have seen before. It wants a quota 276 // increase. There are two circumstances: either the origin 277 // is creating a new database or it has overflowed an existing database. 278 279 // Increase the quota. If estimatedSize == 0, then this is a quota overflow 280 // rather than the creation of a new database. 281 long quotaIncrease = estimatedSize == 0 ? 282 Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota) : 283 estimatedSize; 284 newOriginQuota += quotaIncrease; 285 286 if (quotaIncrease > totalUnusedQuota) { 287 // We can't fit, so deny quota. 288 newOriginQuota = currentQuota; 289 } 290 } 291 292 quotaUpdater.updateQuota(newOriginQuota); 293 294 if(LOGV_ENABLED) { 295 Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to " 296 + newOriginQuota); 297 } 298 } 299 300 /** 301 * The Application Cache has exceeded its max size. 302 * @param spaceNeeded is the amount of disk space that would be needed 303 * in order for the last appcache operation to succeed. 304 * @param totalUsedQuota is the sum of all origins' quota. 305 * @param quotaUpdater A callback to inform the WebCore thread that a new 306 * app cache size is available. This callback must always be executed at 307 * some point to ensure that the sleeping WebCore thread is woken up. 308 */ 309 public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota, 310 WebStorage.QuotaUpdater quotaUpdater) { 311 if(LOGV_ENABLED) { 312 Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded " 313 + spaceNeeded + " bytes."); 314 } 315 316 long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize; 317 318 if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) { 319 // There definitely isn't any more space. Fire notifications 320 // if needed and exit. 321 if (totalUsedQuota > 0) { 322 // We only fire the notification if there are some other websites 323 // using some of the quota. This avoids the degenerate case where 324 // the first ever website to use Web storage tries to use more 325 // data than it is actually available. In such a case, showing 326 // the notification would not help at all since there is nothing 327 // the user can do. 328 scheduleOutOfSpaceNotification(); 329 } 330 quotaUpdater.updateQuota(0); 331 if(LOGV_ENABLED) { 332 Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space."); 333 } 334 return; 335 } 336 // There is enough space to accommodate spaceNeeded bytes. 337 mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING; 338 quotaUpdater.updateQuota(mAppCacheMaxSize); 339 340 if(LOGV_ENABLED) { 341 Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to " 342 + mAppCacheMaxSize); 343 } 344 } 345 346 // Reset the notification time; we use this iff the user 347 // use clear all; we reset it to some time in the future instead 348 // of just setting it to -1, as the clear all method is asynchronous 349 static void resetLastOutOfSpaceNotificationTime() { 350 mLastOutOfSpaceNotificationTime = System.currentTimeMillis() - 351 NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL; 352 } 353 354 // Computes the global limit as a function of the size of the data 355 // partition and the amount of free space on that partition. 356 private long getGlobalLimit() { 357 long freeSpace = mDiskInfo.getFreeSpaceSizeBytes(); 358 long fileSystemSize = mDiskInfo.getTotalSizeBytes(); 359 return calculateGlobalLimit(fileSystemSize, freeSpace); 360 } 361 362 /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes, 363 long freeSpaceBytes) { 364 if (fileSystemSizeBytes <= 0 365 || freeSpaceBytes <= 0 366 || freeSpaceBytes > fileSystemSizeBytes) { 367 return 0; 368 } 369 370 long fileSystemSizeRatio = 371 2 << ((int) Math.floor(Math.log10( 372 fileSystemSizeBytes / (1024 * 1024)))); 373 long maxSizeBytes = (long) Math.min(Math.floor( 374 fileSystemSizeBytes / fileSystemSizeRatio), 375 Math.floor(freeSpaceBytes / 2)); 376 // Round maxSizeBytes up to a multiple of 1024KB (but only if 377 // maxSizeBytes > 1MB). 378 long maxSizeStepBytes = 1024 * 1024; 379 if (maxSizeBytes < maxSizeStepBytes) { 380 return 0; 381 } 382 long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1; 383 return (maxSizeStepBytes 384 * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra)); 385 } 386 387 // Schedules a system notification that takes the user to the WebSettings 388 // activity when clicked. 389 private void scheduleOutOfSpaceNotification() { 390 if(LOGV_ENABLED) { 391 Log.v(LOGTAG, "scheduleOutOfSpaceNotification called."); 392 } 393 if (mContext == null) { 394 // mContext can be null if we're running unit tests. 395 return; 396 } 397 if ((mLastOutOfSpaceNotificationTime == -1) || 398 (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) { 399 // setup the notification boilerplate. 400 int icon = android.R.drawable.stat_sys_warning; 401 CharSequence title = mContext.getString( 402 R.string.webstorage_outofspace_notification_title); 403 CharSequence text = mContext.getString( 404 R.string.webstorage_outofspace_notification_text); 405 long when = System.currentTimeMillis(); 406 Intent intent = new Intent(mContext, WebsiteSettingsActivity.class); 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