summaryrefslogtreecommitdiff
path: root/core/java/android/webkit/CacheManager.java
diff options
context:
space:
mode:
authorThe Android Open Source Project <initial-contribution@android.com>2009-03-03 18:28:45 -0800
committerThe Android Open Source Project <initial-contribution@android.com>2009-03-03 18:28:45 -0800
commitd83a98f4ce9cfa908f5c54bbd70f03eec07e7553 (patch)
tree4b825dc642cb6eb9a060e54bf8d69288fbee4904 /core/java/android/webkit/CacheManager.java
parent076357b8567458d4b6dfdcf839ef751634cd2bfb (diff)
auto import from //depot/cupcake/@135843
Diffstat (limited to 'core/java/android/webkit/CacheManager.java')
-rw-r--r--core/java/android/webkit/CacheManager.java703
1 files changed, 0 insertions, 703 deletions
diff --git a/core/java/android/webkit/CacheManager.java b/core/java/android/webkit/CacheManager.java
deleted file mode 100644
index d12940d04f41..000000000000
--- a/core/java/android/webkit/CacheManager.java
+++ /dev/null
@@ -1,703 +0,0 @@
-/*
- * Copyright (C) 2006 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.webkit;
-
-import android.content.Context;
-import android.net.http.Headers;
-import android.os.FileUtils;
-import android.util.Config;
-import android.util.Log;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Map;
-
-import org.bouncycastle.crypto.Digest;
-import org.bouncycastle.crypto.digests.SHA1Digest;
-
-/**
- * The class CacheManager provides the persistent cache of content that is
- * received over the network. The component handles parsing of HTTP headers and
- * utilizes the relevant cache headers to determine if the content should be
- * stored and if so, how long it is valid for. Network requests are provided to
- * this component and if they can not be resolved by the cache, the HTTP headers
- * are attached, as appropriate, to the request for revalidation of content. The
- * class also manages the cache size.
- */
-public final class CacheManager {
-
- private static final String LOGTAG = "cache";
-
- static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since";
- static final String HEADER_KEY_IFNONEMATCH = "if-none-match";
-
- private static final String NO_STORE = "no-store";
- private static final String NO_CACHE = "no-cache";
- private static final String MAX_AGE = "max-age";
-
- private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
- private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
-
- private static boolean mDisabled;
-
- // Reference count the enable/disable transaction
- private static int mRefCount;
-
- // trimCacheIfNeeded() is called when a page is fully loaded. But JavaScript
- // can load the content, e.g. in a slideshow, continuously, so we need to
- // trim the cache on a timer base too. endCacheTransaction() is called on a
- // timer base. We share the same timer with less frequent update.
- private static int mTrimCacheCount = 0;
- private static final int TRIM_CACHE_INTERVAL = 5;
-
- private static WebViewDatabase mDataBase;
- private static File mBaseDir;
-
- // Flag to clear the cache when the CacheManager is initialized
- private static boolean mClearCacheOnInit = false;
-
- public static class CacheResult {
- // these fields are saved to the database
- int httpStatusCode;
- long contentLength;
- long expires;
- String localPath;
- String lastModified;
- String etag;
- String mimeType;
- String location;
- String encoding;
-
- // these fields are NOT saved to the database
- InputStream inStream;
- OutputStream outStream;
- File outFile;
-
- public int getHttpStatusCode() {
- return httpStatusCode;
- }
-
- public long getContentLength() {
- return contentLength;
- }
-
- public String getLocalPath() {
- return localPath;
- }
-
- public long getExpires() {
- return expires;
- }
-
- public String getLastModified() {
- return lastModified;
- }
-
- public String getETag() {
- return etag;
- }
-
- public String getMimeType() {
- return mimeType;
- }
-
- public String getLocation() {
- return location;
- }
-
- public String getEncoding() {
- return encoding;
- }
-
- // For out-of-package access to the underlying streams.
- public InputStream getInputStream() {
- return inStream;
- }
-
- public OutputStream getOutputStream() {
- return outStream;
- }
-
- // These fields can be set manually.
- public void setInputStream(InputStream stream) {
- this.inStream = stream;
- }
-
- public void setEncoding(String encoding) {
- this.encoding = encoding;
- }
- }
-
- /**
- * initialize the CacheManager. WebView should handle this for each process.
- *
- * @param context The application context.
- */
- static void init(Context context) {
- mDataBase = WebViewDatabase.getInstance(context);
- mBaseDir = new File(context.getCacheDir(), "webviewCache");
- if (createCacheDirectory() && mClearCacheOnInit) {
- removeAllCacheFiles();
- mClearCacheOnInit = false;
- }
- }
-
- /**
- * Create the cache directory if it does not already exist.
- *
- * @return true if the cache directory didn't exist and was created.
- */
- static private boolean createCacheDirectory() {
- if (!mBaseDir.exists()) {
- if(!mBaseDir.mkdirs()) {
- Log.w(LOGTAG, "Unable to create webviewCache directory");
- return false;
- }
- FileUtils.setPermissions(
- mBaseDir.toString(),
- FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
- -1, -1);
- // If we did create the directory, we need to flush
- // the cache database. The directory could be recreated
- // because the system flushed all the data/cache directories
- // to free up disk space.
- WebViewCore.endCacheTransaction();
- mDataBase.clearCache();
- WebViewCore.startCacheTransaction();
- return true;
- }
- return false;
- }
-
- /**
- * get the base directory of the cache. With localPath of the CacheResult,
- * it identifies the cache file.
- *
- * @return File The base directory of the cache.
- */
- public static File getCacheFileBaseDir() {
- return mBaseDir;
- }
-
- /**
- * set the flag to control whether cache is enabled or disabled
- *
- * @param disabled true to disable the cache
- */
- // only called from WebCore thread
- static void setCacheDisabled(boolean disabled) {
- if (disabled == mDisabled) {
- return;
- }
- mDisabled = disabled;
- if (mDisabled) {
- removeAllCacheFiles();
- }
- }
-
- /**
- * get the state of the current cache, enabled or disabled
- *
- * @return return if it is disabled
- */
- public static boolean cacheDisabled() {
- return mDisabled;
- }
-
- // only called from WebCore thread
- // make sure to call enableTransaction/disableTransaction in pair
- static boolean enableTransaction() {
- if (++mRefCount == 1) {
- mDataBase.startCacheTransaction();
- return true;
- }
- return false;
- }
-
- // only called from WebCore thread
- // make sure to call enableTransaction/disableTransaction in pair
- static boolean disableTransaction() {
- if (mRefCount == 0) {
- Log.e(LOGTAG, "disableTransaction is out of sync");
- }
- if (--mRefCount == 0) {
- mDataBase.endCacheTransaction();
- return true;
- }
- return false;
- }
-
- // only called from WebCore thread
- // make sure to call startCacheTransaction/endCacheTransaction in pair
- public static boolean startCacheTransaction() {
- return mDataBase.startCacheTransaction();
- }
-
- // only called from WebCore thread
- // make sure to call startCacheTransaction/endCacheTransaction in pair
- public static boolean endCacheTransaction() {
- boolean ret = mDataBase.endCacheTransaction();
- if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) {
- mTrimCacheCount = 0;
- trimCacheIfNeeded();
- }
- return ret;
- }
-
- /**
- * Given a url, returns the CacheResult if exists. Otherwise returns null.
- * If headers are provided and a cache needs validation,
- * HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE will be set in the
- * cached headers.
- *
- * @return the CacheResult for a given url
- */
- // only called from WebCore thread
- public static CacheResult getCacheFile(String url,
- Map<String, String> headers) {
- if (mDisabled) {
- return null;
- }
-
- CacheResult result = mDataBase.getCache(url);
- if (result != null) {
- if (result.contentLength == 0) {
- if (result.httpStatusCode != 301
- && result.httpStatusCode != 302
- && result.httpStatusCode != 307) {
- // this should not happen. If it does, remove it.
- mDataBase.removeCache(url);
- return null;
- }
- } else {
- File src = new File(mBaseDir, result.localPath);
- try {
- // open here so that even the file is deleted, the content
- // is still readable by the caller until close() is called
- result.inStream = new FileInputStream(src);
- } catch (FileNotFoundException e) {
- // the files in the cache directory can be removed by the
- // system. If it is gone, clean up the database
- mDataBase.removeCache(url);
- return null;
- }
- }
- } else {
- return null;
- }
-
- // null headers request coming from CACHE_MODE_CACHE_ONLY
- // which implies that it needs cache even it is expired.
- // negative expires means time in the far future.
- if (headers != null && result.expires >= 0
- && result.expires <= System.currentTimeMillis()) {
- if (result.lastModified == null && result.etag == null) {
- return null;
- }
- // return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE
- // for requesting validation
- if (result.etag != null) {
- headers.put(HEADER_KEY_IFNONEMATCH, result.etag);
- }
- if (result.lastModified != null) {
- headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified);
- }
- }
-
- if (Config.LOGV) {
- Log.v(LOGTAG, "getCacheFile for url " + url);
- }
-
- return result;
- }
-
- /**
- * Given a url and its full headers, returns CacheResult if a local cache
- * can be stored. Otherwise returns null. The mimetype is passed in so that
- * the function can use the mimetype that will be passed to WebCore which
- * could be different from the mimetype defined in the headers.
- * forceCache is for out-of-package callers to force creation of a
- * CacheResult, and is used to supply surrogate responses for URL
- * interception.
- * @return CacheResult for a given url
- * @hide - hide createCacheFile since it has a parameter of type headers, which is
- * in a hidden package.
- */
- // can be called from any thread
- public static CacheResult createCacheFile(String url, int statusCode,
- Headers headers, String mimeType, boolean forceCache) {
- if (!forceCache && mDisabled) {
- return null;
- }
-
- CacheResult ret = parseHeaders(statusCode, headers, mimeType);
- if (ret != null) {
- setupFiles(url, ret);
- try {
- ret.outStream = new FileOutputStream(ret.outFile);
- } catch (FileNotFoundException e) {
- // This can happen with the system did a purge and our
- // subdirectory has gone, so lets try to create it again
- if (createCacheDirectory()) {
- try {
- ret.outStream = new FileOutputStream(ret.outFile);
- } catch (FileNotFoundException e2) {
- // We failed to create the file again, so there
- // is something else wrong. Return null.
- return null;
- }
- } else {
- // Failed to create cache directory
- return null;
- }
- }
- ret.mimeType = mimeType;
- }
-
- return ret;
- }
-
- /**
- * Save the info of a cache file for a given url to the CacheMap so that it
- * can be reused later
- */
- // only called from WebCore thread
- public static void saveCacheFile(String url, CacheResult cacheRet) {
- try {
- cacheRet.outStream.close();
- } catch (IOException e) {
- return;
- }
-
- if (!cacheRet.outFile.exists()) {
- // the file in the cache directory can be removed by the system
- return;
- }
-
- cacheRet.contentLength = cacheRet.outFile.length();
- if (cacheRet.httpStatusCode == 301
- || cacheRet.httpStatusCode == 302
- || cacheRet.httpStatusCode == 307) {
- // location is in database, no need to keep the file
- cacheRet.contentLength = 0;
- cacheRet.localPath = new String();
- cacheRet.outFile.delete();
- } else if (cacheRet.contentLength == 0) {
- cacheRet.outFile.delete();
- return;
- }
-
- mDataBase.addCache(url, cacheRet);
-
- if (Config.LOGV) {
- Log.v(LOGTAG, "saveCacheFile for url " + url);
- }
- }
-
- /**
- * remove all cache files
- *
- * @return true if it succeeds
- */
- // only called from WebCore thread
- static boolean removeAllCacheFiles() {
- // Note, this is called before init() when the database is
- // created or upgraded.
- if (mBaseDir == null) {
- // Init() has not been called yet, so just flag that
- // we need to clear the cache when init() is called.
- mClearCacheOnInit = true;
- return true;
- }
- // delete cache in a separate thread to not block UI.
- final Runnable clearCache = new Runnable() {
- public void run() {
- // delete all cache files
- try {
- String[] files = mBaseDir.list();
- // if mBaseDir doesn't exist, files can be null.
- if (files != null) {
- for (int i = 0; i < files.length; i++) {
- new File(mBaseDir, files[i]).delete();
- }
- }
- } catch (SecurityException e) {
- // Ignore SecurityExceptions.
- }
- // delete database
- mDataBase.clearCache();
- }
- };
- new Thread(clearCache).start();
- return true;
- }
-
- /**
- * Return true if the cache is empty.
- */
- // only called from WebCore thread
- static boolean cacheEmpty() {
- return mDataBase.hasCache();
- }
-
- // only called from WebCore thread
- static void trimCacheIfNeeded() {
- if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) {
- ArrayList<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT);
- int size = pathList.size();
- for (int i = 0; i < size; i++) {
- new File(mBaseDir, pathList.get(i)).delete();
- }
- }
- }
-
- @SuppressWarnings("deprecation")
- private static void setupFiles(String url, CacheResult cacheRet) {
- if (true) {
- // Note: SHA1 is much stronger hash. But the cost of setupFiles() is
- // 3.2% cpu time for a fresh load of nytimes.com. While a simple
- // String.hashCode() is only 0.6%. If adding the collision resolving
- // to String.hashCode(), it makes the cpu time to be 1.6% for a
- // fresh load, but 5.3% for the worst case where all the files
- // already exist in the file system, but database is gone. So it
- // needs to resolve collision for every file at least once.
- int hashCode = url.hashCode();
- StringBuffer ret = new StringBuffer(8);
- appendAsHex(hashCode, ret);
- String path = ret.toString();
- File file = new File(mBaseDir, path);
- if (true) {
- boolean checkOldPath = true;
- // Check hash collision. If the hash file doesn't exist, just
- // continue. There is a chance that the old cache file is not
- // same as the hash file. As mDataBase.getCache() is more
- // expansive than "leak" a file until clear cache, don't bother.
- // If the hash file exists, make sure that it is same as the
- // cache file. If it is not, resolve the collision.
- while (file.exists()) {
- if (checkOldPath) {
- // as this is called from http thread through
- // createCacheFile, we need endCacheTransaction before
- // database access.
- WebViewCore.endCacheTransaction();
- CacheResult oldResult = mDataBase.getCache(url);
- WebViewCore.startCacheTransaction();
- if (oldResult != null && oldResult.contentLength > 0) {
- if (path.equals(oldResult.localPath)) {
- path = oldResult.localPath;
- } else {
- path = oldResult.localPath;
- file = new File(mBaseDir, path);
- }
- break;
- }
- checkOldPath = false;
- }
- ret = new StringBuffer(8);
- appendAsHex(++hashCode, ret);
- path = ret.toString();
- file = new File(mBaseDir, path);
- }
- }
- cacheRet.localPath = path;
- cacheRet.outFile = file;
- } else {
- // get hash in byte[]
- Digest digest = new SHA1Digest();
- int digestLen = digest.getDigestSize();
- byte[] hash = new byte[digestLen];
- int urlLen = url.length();
- byte[] data = new byte[urlLen];
- url.getBytes(0, urlLen, data, 0);
- digest.update(data, 0, urlLen);
- digest.doFinal(hash, 0);
- // convert byte[] to hex String
- StringBuffer result = new StringBuffer(2 * digestLen);
- for (int i = 0; i < digestLen; i = i + 4) {
- int h = (0x00ff & hash[i]) << 24 | (0x00ff & hash[i + 1]) << 16
- | (0x00ff & hash[i + 2]) << 8 | (0x00ff & hash[i + 3]);
- appendAsHex(h, result);
- }
- cacheRet.localPath = result.toString();
- cacheRet.outFile = new File(mBaseDir, cacheRet.localPath);
- }
- }
-
- private static void appendAsHex(int i, StringBuffer ret) {
- String hex = Integer.toHexString(i);
- switch (hex.length()) {
- case 1:
- ret.append("0000000");
- break;
- case 2:
- ret.append("000000");
- break;
- case 3:
- ret.append("00000");
- break;
- case 4:
- ret.append("0000");
- break;
- case 5:
- ret.append("000");
- break;
- case 6:
- ret.append("00");
- break;
- case 7:
- ret.append("0");
- break;
- }
- ret.append(hex);
- }
-
- private static CacheResult parseHeaders(int statusCode, Headers headers,
- String mimeType) {
- // TODO: if authenticated or secure, return null
- CacheResult ret = new CacheResult();
- ret.httpStatusCode = statusCode;
-
- String location = headers.getLocation();
- if (location != null) ret.location = location;
-
- ret.expires = -1;
- String expires = headers.getExpires();
- if (expires != null) {
- try {
- ret.expires = HttpDateTime.parse(expires);
- } catch (IllegalArgumentException ex) {
- // Take care of the special "-1" and "0" cases
- if ("-1".equals(expires) || "0".equals(expires)) {
- // make it expired, but can be used for history navigation
- ret.expires = 0;
- } else {
- Log.e(LOGTAG, "illegal expires: " + expires);
- }
- }
- }
-
- String lastModified = headers.getLastModified();
- if (lastModified != null) ret.lastModified = lastModified;
-
- String etag = headers.getEtag();
- if (etag != null) ret.etag = etag;
-
- String cacheControl = headers.getCacheControl();
- if (cacheControl != null) {
- String[] controls = cacheControl.toLowerCase().split("[ ,;]");
- for (int i = 0; i < controls.length; i++) {
- if (NO_STORE.equals(controls[i])) {
- return null;
- }
- // According to the spec, 'no-cache' means that the content
- // must be re-validated on every load. It does not mean that
- // the content can not be cached. set to expire 0 means it
- // can only be used in CACHE_MODE_CACHE_ONLY case
- if (NO_CACHE.equals(controls[i])) {
- ret.expires = 0;
- } else if (controls[i].startsWith(MAX_AGE)) {
- int separator = controls[i].indexOf('=');
- if (separator < 0) {
- separator = controls[i].indexOf(':');
- }
- if (separator > 0) {
- String s = controls[i].substring(separator + 1);
- try {
- long sec = Long.parseLong(s);
- if (sec >= 0) {
- ret.expires = System.currentTimeMillis() + 1000
- * sec;
- }
- } catch (NumberFormatException ex) {
- if ("1d".equals(s)) {
- // Take care of the special "1d" case
- ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
- } else {
- Log.e(LOGTAG, "exception in parseHeaders for "
- + "max-age:"
- + controls[i].substring(separator + 1));
- ret.expires = 0;
- }
- }
- }
- }
- }
- }
-
- // According to RFC 2616 section 14.32:
- // HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the
- // client had sent "Cache-Control: no-cache"
- if (NO_CACHE.equals(headers.getPragma())) {
- ret.expires = 0;
- }
-
- // According to RFC 2616 section 13.2.4, if an expiration has not been
- // explicitly defined a heuristic to set an expiration may be used.
- if (ret.expires == -1) {
- if (ret.httpStatusCode == 301) {
- // If it is a permanent redirect, and it did not have an
- // explicit cache directive, then it never expires
- ret.expires = Long.MAX_VALUE;
- } else if (ret.httpStatusCode == 302 || ret.httpStatusCode == 307) {
- // If it is temporary redirect, expires
- ret.expires = 0;
- } else if (ret.lastModified == null) {
- // When we have no last-modified, then expire the content with
- // in 24hrs as, according to the RFC, longer time requires a
- // warning 113 to be added to the response.
-
- // Only add the default expiration for non-html markup. Some
- // sites like news.google.com have no cache directives.
- if (!mimeType.startsWith("text/html")) {
- ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
- } else {
- // Setting a expires as zero will cache the result for
- // forward/back nav.
- ret.expires = 0;
- }
- } else {
- // If we have a last-modified value, we could use it to set the
- // expiration. Suggestion from RFC is 10% of time since
- // last-modified. As we are on mobile, loads are expensive,
- // increasing this to 20%.
-
- // 24 * 60 * 60 * 1000
- long lastmod = System.currentTimeMillis() + 86400000;
- try {
- lastmod = HttpDateTime.parse(ret.lastModified);
- } catch (IllegalArgumentException ex) {
- Log.e(LOGTAG, "illegal lastModified: " + ret.lastModified);
- }
- long difference = System.currentTimeMillis() - lastmod;
- if (difference > 0) {
- ret.expires = System.currentTimeMillis() + difference / 5;
- } else {
- // last modified is in the future, expire the content
- // on the last modified
- ret.expires = lastmod;
- }
- }
- }
-
- return ret;
- }
-}