/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. package com.marvin.webvox;
  17. import com.marvin.webvox.R;
  18. import android.app.Notification;
  19. import android.app.NotificationManager;
  20. import android.app.PendingIntent;
  21. import android.content.Context;
  22. import android.content.Intent;
  23. import android.os.StatFs;
  24. import android.util.Log;
  25. import android.webkit.WebStorage;
  26. import java.io.File;
  27. import java.util.Set;
  28. /**
  29. * Package level class for managing the disk size consumed by the WebDatabase
  30. * and ApplicationCaches APIs (henceforth called Web storage).
  31. *
  32. * Currently, the situation on the WebKit side is as follows:
  33. * - WebDatabase enforces a quota for each origin.
  34. * - Session/LocalStorage do not enforce any disk limits.
  35. * - ApplicationCaches enforces a maximum size for all origins.
  36. *
  37. * The WebStorageSizeManager maintains a global limit for the disk space
  38. * consumed by the WebDatabase and ApplicationCaches. As soon as WebKit will
  39. * have a limit for Session/LocalStorage, this class will manage the space used
  40. * by those APIs as well.
  41. *
  42. * The global limit is computed as a function of the size of the partition where
  43. * these APIs store their data (they must store it on the same partition for
  44. * this to work) and the size of the available space on that partition.
  45. * The global limit is not subject to user configuration but we do provide
  46. * a debug-only setting.
  47. * TODO(andreip): implement the debug setting.
  48. *
  49. * The size of the disk space used for Web storage is initially divided between
  50. * WebDatabase and ApplicationCaches as follows:
  51. *
  52. * 75% for WebDatabase
  53. * 25% for ApplicationCaches
  54. *
  55. * When an origin's database usage reaches its current quota, WebKit invokes
  56. * the following callback function:
  57. * - exceededDatabaseQuota(Frame* frame, const String& database_name);
  58. * Note that the default quota for a new origin is 0, so we will receive the
  59. * 'exceededDatabaseQuota' callback before a new origin gets the chance to
  60. * create its first database.
  61. *
  62. * When the total ApplicationCaches usage reaches its current quota, WebKit
  63. * invokes the following callback function:
  64. * - void reachedMaxAppCacheSize(int64_t spaceNeeded);
  65. *
  66. * The WebStorageSizeManager's main job is to respond to the above two callbacks
  67. * by inspecting the amount of unused Web storage quota (i.e. global limit -
  68. * sum of all other origins' quota) and deciding if a quota increase for the
  69. * out-of-space origin is allowed or not.
  70. *
  71. * The default quota for an origin is its estimated size. If we cannot satisfy
  72. * the estimated size, then WebCore will not create the database.
  73. * Quota increases are done in steps, where the increase step is
  74. * min(QUOTA_INCREASE_STEP, unused_quota).
  75. *
  76. * When all the Web storage space is used, the WebStorageSizeManager creates
  77. * a system notification that will guide the user to the WebSettings UI. There,
  78. * the user can free some of the Web storage space by deleting all the data used
  79. * by an origin.
  80. */
  81. class WebStorageSizeManager {
  82. // Logging flags.
  83. private final static boolean LOGV_ENABLED = com.marvin.webvox.Browser.LOGV_ENABLED;
  84. private final static boolean LOGD_ENABLED = com.marvin.webvox.Browser.LOGD_ENABLED;
  85. private final static String LOGTAG = "browser";
  86. // The default quota value for an origin.
  87. public final static long ORIGIN_DEFAULT_QUOTA = 3 * 1024 * 1024; // 3MB
  88. // The default value for quota increases.
  89. public final static long QUOTA_INCREASE_STEP = 1 * 1024 * 1024; // 1MB
  90. // Extra padding space for appcache maximum size increases. This is needed
  91. // because WebKit sends us an estimate of the amount of space needed
  92. // but this estimate may, currently, be slightly less than what is actually
  93. // needed. We therefore add some 'padding'.
  94. // TODO(andreip): fix this in WebKit.
  95. public final static long APPCACHE_MAXSIZE_PADDING = 512 * 1024; // 512KB
  96. // The system status bar notification id.
  97. private final static int OUT_OF_SPACE_ID = 1;
  98. // The time of the last out of space notification
  99. private static long mLastOutOfSpaceNotificationTime = -1;
  100. // Delay between two notification in ms
  101. private final static long NOTIFICATION_INTERVAL = 5 * 60 * 1000;
  102. // Delay in ms used when resetting the notification time
  103. private final static long RESET_NOTIFICATION_INTERVAL = 3 * 1000;
  104. // The application context.
  105. private final Context mContext;
  106. // The global Web storage limit.
  107. private final long mGlobalLimit;
  108. // The maximum size of the application cache file.
  109. private long mAppCacheMaxSize;
  110. /**
  111. * Interface used by the WebStorageSizeManager to obtain information
  112. * about the underlying file system. This functionality is separated
  113. * into its own interface mainly for testing purposes.
  114. */
  115. public interface DiskInfo {
  116. /**
  117. * @return the size of the free space in the file system.
  118. */
  119. public long getFreeSpaceSizeBytes();
  120. /**
  121. * @return the total size of the file system.
  122. */
  123. public long getTotalSizeBytes();
  124. };
  125. private DiskInfo mDiskInfo;
  126. // For convenience, we provide a DiskInfo implementation that uses StatFs.
  127. public static class StatFsDiskInfo implements DiskInfo {
  128. private StatFs mFs;
  129. public StatFsDiskInfo(String path) {
  130. mFs = new StatFs(path);
  131. }
  132. public long getFreeSpaceSizeBytes() {
  133. return mFs.getAvailableBlocks() * mFs.getBlockSize();
  134. }
  135. public long getTotalSizeBytes() {
  136. return mFs.getBlockCount() * mFs.getBlockSize();
  137. }
  138. };
  139. /**
  140. * Interface used by the WebStorageSizeManager to obtain information
  141. * about the appcache file. This functionality is separated into its own
  142. * interface mainly for testing purposes.
  143. */
  144. public interface AppCacheInfo {
  145. /**
  146. * @return the current size of the appcache file.
  147. */
  148. public long getAppCacheSizeBytes();
  149. };
  150. // For convenience, we provide an AppCacheInfo implementation.
  151. public static class WebKitAppCacheInfo implements AppCacheInfo {
  152. // The name of the application cache file. Keep in sync with
  153. // WebCore/loader/appcache/ApplicationCacheStorage.cpp
  154. private final static String APPCACHE_FILE = "ApplicationCache.db";
  155. private String mAppCachePath;
  156. public WebKitAppCacheInfo(String path) {
  157. mAppCachePath = path;
  158. }
  159. public long getAppCacheSizeBytes() {
  160. File file = new File(mAppCachePath
  161. + File.separator
  162. + APPCACHE_FILE);
  163. return file.length();
  164. }
  165. };
  166. /**
  167. * Public ctor
  168. * @param ctx is the application context
  169. * @param diskInfo is the DiskInfo instance used to query the file system.
  170. * @param appCacheInfo is the AppCacheInfo used to query info about the
  171. * appcache file.
  172. */
  173. public WebStorageSizeManager(Context ctx, DiskInfo diskInfo,
  174. AppCacheInfo appCacheInfo) {
  175. mContext = ctx;
  176. mDiskInfo = diskInfo;
  177. mGlobalLimit = getGlobalLimit();
  178. // The initial max size of the app cache is either 25% of the global
  179. // limit or the current size of the app cache file, whichever is bigger.
  180. mAppCacheMaxSize = Math.max(mGlobalLimit / 4,
  181. appCacheInfo.getAppCacheSizeBytes());
  182. }
  183. /**
  184. * Returns the maximum size of the application cache.
  185. */
  186. public long getAppCacheMaxSize() {
  187. return mAppCacheMaxSize;
  188. }
  189. /**
  190. * The origin has exceeded its database quota.
  191. * @param url the URL that exceeded the quota
  192. * @param databaseIdentifier the identifier of the database on
  193. * which the transaction that caused the quota overflow was run
  194. * @param currentQuota the current quota for the origin.
  195. * @param totalUsedQuota is the sum of all origins' quota.
  196. * @param quotaUpdater The callback to run when a decision to allow or
  197. * deny quota has been made. Don't forget to call this!
  198. */
  199. public void onExceededDatabaseQuota(String url,
  200. String databaseIdentifier, long currentQuota, long estimatedSize,
  201. long totalUsedQuota, WebStorage.QuotaUpdater quotaUpdater) {
  202. if(LOGV_ENABLED) {
  203. Log.v(LOGTAG,
  204. "Received onExceededDatabaseQuota for "
  205. + url
  206. + ":"
  207. + databaseIdentifier
  208. + "(current quota: "
  209. + currentQuota
  210. + ", total used quota: "
  211. + totalUsedQuota
  212. + ")");
  213. }
  214. long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
  215. if (totalUnusedQuota <= 0) {
  216. // There definitely isn't any more space. Fire notifications
  217. // if needed and exit.
  218. if (totalUsedQuota > 0) {
  219. // We only fire the notification if there are some other websites
  220. // using some of the quota. This avoids the degenerate case where
  221. // the first ever website to use Web storage tries to use more
  222. // data than it is actually available. In such a case, showing
  223. // the notification would not help at all since there is nothing
  224. // the user can do.
  225. scheduleOutOfSpaceNotification();
  226. }
  227. quotaUpdater.updateQuota(currentQuota);
  228. if(LOGV_ENABLED) {
  229. Log.v(LOGTAG, "onExceededDatabaseQuota: out of space.");
  230. }
  231. return;
  232. }
  233. // We have enough space inside mGlobalLimit.
  234. long newOriginQuota = currentQuota;
  235. if (newOriginQuota == 0) {
  236. // This is a new origin, give it the size it asked for if possible.
  237. // If we cannot satisfy the estimatedSize, we should return 0 as
  238. // returning a value less that what the site requested will lead
  239. // to webcore not creating the database.
  240. if (totalUnusedQuota >= estimatedSize) {
  241. newOriginQuota = estimatedSize;
  242. } else {
  243. if (LOGV_ENABLED) {
  244. Log.v(LOGTAG,
  245. "onExceededDatabaseQuota: Unable to satisfy" +
  246. " estimatedSize for the new database " +
  247. " (estimatedSize: " + estimatedSize +
  248. ", unused quota: " + totalUnusedQuota);
  249. }
  250. newOriginQuota = 0;
  251. }
  252. } else {
  253. // This is an origin we have seen before. It wants a quota
  254. // increase.
  255. newOriginQuota +=
  256. Math.min(QUOTA_INCREASE_STEP, totalUnusedQuota);
  257. }
  258. quotaUpdater.updateQuota(newOriginQuota);
  259. if(LOGV_ENABLED) {
  260. Log.v(LOGTAG, "onExceededDatabaseQuota set new quota to "
  261. + newOriginQuota);
  262. }
  263. }
  264. /**
  265. * The Application Cache has exceeded its max size.
  266. * @param spaceNeeded is the amount of disk space that would be needed
  267. * in order for the last appcache operation to succeed.
  268. * @param totalUsedQuota is the sum of all origins' quota.
  269. * @param quotaUpdater A callback to inform the WebCore thread that a new
  270. * app cache size is available. This callback must always be executed at
  271. * some point to ensure that the sleeping WebCore thread is woken up.
  272. */
  273. public void onReachedMaxAppCacheSize(long spaceNeeded, long totalUsedQuota,
  274. WebStorage.QuotaUpdater quotaUpdater) {
  275. if(LOGV_ENABLED) {
  276. Log.v(LOGTAG, "Received onReachedMaxAppCacheSize with spaceNeeded "
  277. + spaceNeeded + " bytes.");
  278. }
  279. long totalUnusedQuota = mGlobalLimit - totalUsedQuota - mAppCacheMaxSize;
  280. if (totalUnusedQuota < spaceNeeded + APPCACHE_MAXSIZE_PADDING) {
  281. // There definitely isn't any more space. Fire notifications
  282. // if needed and exit.
  283. if (totalUsedQuota > 0) {
  284. // We only fire the notification if there are some other websites
  285. // using some of the quota. This avoids the degenerate case where
  286. // the first ever website to use Web storage tries to use more
  287. // data than it is actually available. In such a case, showing
  288. // the notification would not help at all since there is nothing
  289. // the user can do.
  290. scheduleOutOfSpaceNotification();
  291. }
  292. quotaUpdater.updateQuota(0);
  293. if(LOGV_ENABLED) {
  294. Log.v(LOGTAG, "onReachedMaxAppCacheSize: out of space.");
  295. }
  296. return;
  297. }
  298. // There is enough space to accommodate spaceNeeded bytes.
  299. mAppCacheMaxSize += spaceNeeded + APPCACHE_MAXSIZE_PADDING;
  300. quotaUpdater.updateQuota(mAppCacheMaxSize);
  301. if(LOGV_ENABLED) {
  302. Log.v(LOGTAG, "onReachedMaxAppCacheSize set new max size to "
  303. + mAppCacheMaxSize);
  304. }
  305. }
  306. // Reset the notification time; we use this iff the user
  307. // use clear all; we reset it to some time in the future instead
  308. // of just setting it to -1, as the clear all method is asynchronous
  309. static void resetLastOutOfSpaceNotificationTime() {
  310. mLastOutOfSpaceNotificationTime = System.currentTimeMillis() -
  311. NOTIFICATION_INTERVAL + RESET_NOTIFICATION_INTERVAL;
  312. }
  313. // Computes the global limit as a function of the size of the data
  314. // partition and the amount of free space on that partition.
  315. private long getGlobalLimit() {
  316. long freeSpace = mDiskInfo.getFreeSpaceSizeBytes();
  317. long fileSystemSize = mDiskInfo.getTotalSizeBytes();
  318. return calculateGlobalLimit(fileSystemSize, freeSpace);
  319. }
  320. /*package*/ static long calculateGlobalLimit(long fileSystemSizeBytes,
  321. long freeSpaceBytes) {
  322. if (fileSystemSizeBytes <= 0
  323. || freeSpaceBytes <= 0
  324. || freeSpaceBytes > fileSystemSizeBytes) {
  325. return 0;
  326. }
  327. long fileSystemSizeRatio =
  328. 2 << ((int) Math.floor(Math.log10(
  329. fileSystemSizeBytes / (1024 * 1024))));
  330. long maxSizeBytes = (long) Math.min(Math.floor(
  331. fileSystemSizeBytes / fileSystemSizeRatio),
  332. Math.floor(freeSpaceBytes / 2));
  333. // Round maxSizeBytes up to a multiple of 1024KB (but only if
  334. // maxSizeBytes > 1MB).
  335. long maxSizeStepBytes = 1024 * 1024;
  336. if (maxSizeBytes < maxSizeStepBytes) {
  337. return 0;
  338. }
  339. long roundingExtra = maxSizeBytes % maxSizeStepBytes == 0 ? 0 : 1;
  340. return (maxSizeStepBytes
  341. * ((maxSizeBytes / maxSizeStepBytes) + roundingExtra));
  342. }
  343. // Schedules a system notification that takes the user to the WebSettings
  344. // activity when clicked.
  345. private void scheduleOutOfSpaceNotification() {
  346. if(LOGV_ENABLED) {
  347. Log.v(LOGTAG, "scheduleOutOfSpaceNotification called.");
  348. }
  349. if (mContext == null) {
  350. // mContext can be null if we're running unit tests.
  351. return;
  352. }
  353. if ((mLastOutOfSpaceNotificationTime == -1) ||
  354. (System.currentTimeMillis() - mLastOutOfSpaceNotificationTime > NOTIFICATION_INTERVAL)) {
  355. // setup the notification boilerplate.
  356. int icon = android.R.drawable.stat_sys_warning;
  357. CharSequence title = mContext.getString(
  358. R.string.webstorage_outofspace_notification_title);
  359. CharSequence text = mContext.getString(
  360. R.string.webstorage_outofspace_notification_text);
  361. long when = System.currentTimeMillis();
  362. Intent intent = new Intent(mContext, WebsiteSettingsActivity.class);
  363. PendingIntent contentIntent =
  364. PendingIntent.getActivity(mContext, 0, intent, 0);
  365. Notification notification = new Notification(icon, title, when);
  366. notification.setLatestEventInfo(mContext, title, text, contentIntent);
  367. notification.flags |= Notification.FLAG_AUTO_CANCEL;
  368. // Fire away.
  369. String ns = Context.NOTIFICATION_SERVICE;
  370. NotificationManager mgr =
  371. (NotificationManager) mContext.getSystemService(ns);
  372. if (mgr != null) {
  373. mLastOutOfSpaceNotificationTime = System.currentTimeMillis();
  374. mgr.notify(OUT_OF_SPACE_ID, notification);
  375. }
  376. }
  377. }
  378. }