2 * Copyright (C) 2006 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package android.webkit;
19 import android.content.Context;
20 import android.net.http.AndroidHttpClient;
21 import android.net.http.Headers;
22 import android.os.FileUtils;
23 import android.util.Log;
25 import java.io.FileInputStream;
26 import java.io.FileNotFoundException;
27 import java.io.FileOutputStream;
28 import java.io.FilenameFilter;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.util.List;
36 import org.bouncycastle.crypto.Digest;
37 import org.bouncycastle.crypto.digests.SHA1Digest;
40 * The class CacheManager provides the persistent cache of content that is
41 * received over the network. The component handles parsing of HTTP headers and
42 * utilizes the relevant cache headers to determine if the content should be
43 * stored and if so, how long it is valid for. Network requests are provided to
44 * this component and if they can not be resolved by the cache, the HTTP headers
45 * are attached, as appropriate, to the request for revalidation of content. The
46 * class also manages the cache size.
48 * @deprecated Access to the HTTP cache will be removed in a future release.
51 public final class CacheManager {
53 private static final String LOGTAG = "cache";
55 static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since";
56 static final String HEADER_KEY_IFNONEMATCH = "if-none-match";
58 private static final String NO_STORE = "no-store";
59 private static final String NO_CACHE = "no-cache";
60 private static final String MAX_AGE = "max-age";
61 private static final String MANIFEST_MIME = "text/cache-manifest";
63 private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
64 private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
66 // Limit the maximum cache file size to half of the normal capacity
67 static long CACHE_MAX_SIZE = (CACHE_THRESHOLD - CACHE_TRIM_AMOUNT) / 2;
69 private static boolean mDisabled;
71 // Reference count the enable/disable transaction
72 private static int mRefCount;
74 // trimCacheIfNeeded() is called when a page is fully loaded. But JavaScript
75 // can load the content, e.g. in a slideshow, continuously, so we need to
76 // trim the cache on a timer base too. endCacheTransaction() is called on a
77 // timer base. We share the same timer with less frequent update.
78 private static int mTrimCacheCount = 0;
79 private static final int TRIM_CACHE_INTERVAL = 5;
81 private static WebViewDatabase mDataBase;
82 private static File mBaseDir;
84 // Flag to clear the cache when the CacheManager is initialized
85 private static boolean mClearCacheOnInit = false;
88 * This class represents a resource retrieved from the HTTP cache.
89 * Instances of this class can be obtained by invoking the
90 * CacheManager.getCacheFile() method.
92 * @deprecated Access to the HTTP cache will be removed in a future release.
95 public static class CacheResult {
96 // these fields are saved to the database
100 String expiresString;
107 String contentdisposition;
110 // these fields are NOT saved to the database
111 InputStream inStream;
112 OutputStream outStream;
115 public int getHttpStatusCode() {
116 return httpStatusCode;
119 public long getContentLength() {
120 return contentLength;
123 public String getLocalPath() {
127 public long getExpires() {
131 public String getExpiresString() {
132 return expiresString;
135 public String getLastModified() {
139 public String getETag() {
143 public String getMimeType() {
147 public String getLocation() {
151 public String getEncoding() {
155 public String getContentDisposition() {
156 return contentdisposition;
159 // For out-of-package access to the underlying streams.
160 public InputStream getInputStream() {
164 public OutputStream getOutputStream() {
168 // These fields can be set manually.
169 public void setInputStream(InputStream stream) {
170 this.inStream = stream;
173 public void setEncoding(String encoding) {
174 this.encoding = encoding;
180 public void setContentLength(long contentLength) {
181 this.contentLength = contentLength;
186 * Initialize the CacheManager.
188 * Note that this is called automatically when a {@link android.webkit.WebView} is created.
190 * @param context The application context.
192 static void init(Context context) {
193 if (JniUtil.useChromiumHttpStack()) {
194 // TODO: Need to init mBaseDir.
198 mDataBase = WebViewDatabase.getInstance(context.getApplicationContext());
199 mBaseDir = new File(context.getCacheDir(), "webviewCache");
200 if (createCacheDirectory() && mClearCacheOnInit) {
201 removeAllCacheFiles();
202 mClearCacheOnInit = false;
207 * Create the cache directory if it does not already exist.
209 * @return true if the cache directory didn't exist and was created.
211 static private boolean createCacheDirectory() {
212 assert !JniUtil.useChromiumHttpStack();
214 if (!mBaseDir.exists()) {
215 if(!mBaseDir.mkdirs()) {
216 Log.w(LOGTAG, "Unable to create webviewCache directory");
219 FileUtils.setPermissions(
221 FileUtils.S_IRWXU | FileUtils.S_IRWXG,
223 // If we did create the directory, we need to flush
224 // the cache database. The directory could be recreated
225 // because the system flushed all the data/cache directories
226 // to free up disk space.
227 // delete rows in the cache database
228 WebViewWorker.getHandler().sendEmptyMessage(
229 WebViewWorker.MSG_CLEAR_CACHE);
236 * Get the base directory of the cache. Together with the local path of the CacheResult,
237 * obtained from {@link android.webkit.CacheManager.CacheResult#getLocalPath}, this
238 * identifies the cache file.
240 * @return File The base directory of the cache.
242 * @deprecated Access to the HTTP cache will be removed in a future release.
245 public static File getCacheFileBaseDir() {
250 * Sets whether the cache is disabled.
252 * @param disabled Whether the cache should be disabled
254 static void setCacheDisabled(boolean disabled) {
255 assert !JniUtil.useChromiumHttpStack();
257 if (disabled == mDisabled) {
260 mDisabled = disabled;
262 removeAllCacheFiles();
267 * Whether the cache is disabled.
269 * @return return Whether the cache is disabled
271 * @deprecated Access to the HTTP cache will be removed in a future release.
274 public static boolean cacheDisabled() {
278 // only called from WebViewWorkerThread
279 // make sure to call enableTransaction/disableTransaction in pair
280 static boolean enableTransaction() {
281 assert !JniUtil.useChromiumHttpStack();
283 if (++mRefCount == 1) {
284 mDataBase.startCacheTransaction();
290 // only called from WebViewWorkerThread
291 // make sure to call enableTransaction/disableTransaction in pair
292 static boolean disableTransaction() {
293 assert !JniUtil.useChromiumHttpStack();
295 if (--mRefCount == 0) {
296 mDataBase.endCacheTransaction();
302 // only called from WebViewWorkerThread
303 // make sure to call startTransaction/endTransaction in pair
304 static boolean startTransaction() {
305 assert !JniUtil.useChromiumHttpStack();
307 return mDataBase.startCacheTransaction();
310 // only called from WebViewWorkerThread
311 // make sure to call startTransaction/endTransaction in pair
312 static boolean endTransaction() {
313 assert !JniUtil.useChromiumHttpStack();
315 boolean ret = mDataBase.endCacheTransaction();
316 if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) {
323 // only called from WebCore Thread
324 // make sure to call startCacheTransaction/endCacheTransaction in pair
326 * @deprecated Always returns false.
329 public static boolean startCacheTransaction() {
333 // only called from WebCore Thread
334 // make sure to call startCacheTransaction/endCacheTransaction in pair
336 * @deprecated Always returns false.
339 public static boolean endCacheTransaction() {
344 * Given a URL, returns the corresponding CacheResult if it exists, or null otherwise.
346 * The output stream of the CacheEntry object is initialized and opened and should be closed by
347 * the caller when access to the undelying file is no longer required.
348 * If a non-zero value is provided for the headers map, and the cache entry needs validation,
349 * HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE will be set in headers.
351 * @return The CacheResult for the given URL
353 * @deprecated Access to the HTTP cache will be removed in a future release.
356 public static CacheResult getCacheFile(String url,
357 Map<String, String> headers) {
358 return getCacheFile(url, 0, headers);
361 static CacheResult getCacheFile(String url, long postIdentifier,
362 Map<String, String> headers) {
367 if (JniUtil.useChromiumHttpStack()) {
368 // TODO: Implement this.
372 String databaseKey = getDatabaseKey(url, postIdentifier);
373 CacheResult result = mDataBase.getCache(databaseKey);
374 if (result == null) {
377 if (result.contentLength == 0) {
378 if (!isCachableRedirect(result.httpStatusCode)) {
379 // This should not happen. If it does, remove it.
380 mDataBase.removeCache(databaseKey);
384 File src = new File(mBaseDir, result.localPath);
386 // Open the file here so that even if it is deleted, the content
387 // is still readable by the caller until close() is called.
388 result.inStream = new FileInputStream(src);
389 } catch (FileNotFoundException e) {
390 // The files in the cache directory can be removed by the
391 // system. If it is gone, clean up the database.
392 mDataBase.removeCache(databaseKey);
397 // A null value for headers is used by CACHE_MODE_CACHE_ONLY to imply
398 // that we should provide the cache result even if it is expired.
399 // Note that a negative expires value means a time in the far future.
400 if (headers != null && result.expires >= 0
401 && result.expires <= System.currentTimeMillis()) {
402 if (result.lastModified == null && result.etag == null) {
405 // Return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE
406 // for requesting validation.
407 if (result.etag != null) {
408 headers.put(HEADER_KEY_IFNONEMATCH, result.etag);
410 if (result.lastModified != null) {
411 headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified);
415 if (DebugFlags.CACHE_MANAGER) {
416 Log.v(LOGTAG, "getCacheFile for url " + url);
423 * Given a url and its full headers, returns CacheResult if a local cache
424 * can be stored. Otherwise returns null. The mimetype is passed in so that
425 * the function can use the mimetype that will be passed to WebCore which
426 * could be different from the mimetype defined in the headers.
427 * forceCache is for out-of-package callers to force creation of a
428 * CacheResult, and is used to supply surrogate responses for URL
430 * @return CacheResult for a given url
431 * @hide - hide createCacheFile since it has a parameter of type headers, which is
432 * in a hidden package.
434 * @deprecated Access to the HTTP cache will be removed in a future release.
437 public static CacheResult createCacheFile(String url, int statusCode,
438 Headers headers, String mimeType, boolean forceCache) {
439 if (JniUtil.useChromiumHttpStack()) {
440 // TODO: Implement this.
444 return createCacheFile(url, statusCode, headers, mimeType, 0,
448 static CacheResult createCacheFile(String url, int statusCode,
449 Headers headers, String mimeType, long postIdentifier,
450 boolean forceCache) {
451 assert !JniUtil.useChromiumHttpStack();
453 if (!forceCache && mDisabled) {
457 String databaseKey = getDatabaseKey(url, postIdentifier);
459 // according to the rfc 2616, the 303 response MUST NOT be cached.
460 if (statusCode == 303) {
461 // remove the saved cache if there is any
462 mDataBase.removeCache(databaseKey);
466 // like the other browsers, do not cache redirects containing a cookie
468 if (isCachableRedirect(statusCode) && !headers.getSetCookie().isEmpty()) {
469 // remove the saved cache if there is any
470 mDataBase.removeCache(databaseKey);
474 CacheResult ret = parseHeaders(statusCode, headers, mimeType);
476 // this should only happen if the headers has "no-store" in the
477 // cache-control. remove the saved cache if there is any
478 mDataBase.removeCache(databaseKey);
480 setupFiles(databaseKey, ret);
482 ret.outStream = new FileOutputStream(ret.outFile);
483 } catch (FileNotFoundException e) {
484 // This can happen with the system did a purge and our
485 // subdirectory has gone, so lets try to create it again
486 if (createCacheDirectory()) {
488 ret.outStream = new FileOutputStream(ret.outFile);
489 } catch (FileNotFoundException e2) {
490 // We failed to create the file again, so there
491 // is something else wrong. Return null.
495 // Failed to create cache directory
499 ret.mimeType = mimeType;
506 * Save the info of a cache file for a given url to the CacheMap so that it
507 * can be reused later
509 * @deprecated Access to the HTTP cache will be removed in a future release.
512 public static void saveCacheFile(String url, CacheResult cacheRet) {
513 saveCacheFile(url, 0, cacheRet);
516 static void saveCacheFile(String url, long postIdentifier,
517 CacheResult cacheRet) {
519 cacheRet.outStream.close();
520 } catch (IOException e) {
524 if (JniUtil.useChromiumHttpStack()) {
525 // TODO: Implement this.
529 if (!cacheRet.outFile.exists()) {
530 // the file in the cache directory can be removed by the system
534 boolean redirect = isCachableRedirect(cacheRet.httpStatusCode);
536 // location is in database, no need to keep the file
537 cacheRet.contentLength = 0;
538 cacheRet.localPath = "";
540 if ((redirect || cacheRet.contentLength == 0)
541 && !cacheRet.outFile.delete()) {
542 Log.e(LOGTAG, cacheRet.outFile.getPath() + " delete failed.");
544 if (cacheRet.contentLength == 0) {
548 mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet);
550 if (DebugFlags.CACHE_MANAGER) {
551 Log.v(LOGTAG, "saveCacheFile for url " + url);
555 static boolean cleanupCacheFile(CacheResult cacheRet) {
556 assert !JniUtil.useChromiumHttpStack();
559 cacheRet.outStream.close();
560 } catch (IOException e) {
563 return cacheRet.outFile.delete();
567 * Remove all cache files.
569 * @return Whether the removal succeeded.
571 static boolean removeAllCacheFiles() {
572 assert !JniUtil.useChromiumHttpStack();
574 // Note, this is called before init() when the database is
575 // created or upgraded.
576 if (mBaseDir == null) {
577 // Init() has not been called yet, so just flag that
578 // we need to clear the cache when init() is called.
579 mClearCacheOnInit = true;
582 // delete rows in the cache database
583 WebViewWorker.getHandler().sendEmptyMessage(
584 WebViewWorker.MSG_CLEAR_CACHE);
585 // delete cache files in a separate thread to not block UI.
586 final Runnable clearCache = new Runnable() {
588 // delete all cache files
590 String[] files = mBaseDir.list();
591 // if mBaseDir doesn't exist, files can be null.
593 for (int i = 0; i < files.length; i++) {
594 File f = new File(mBaseDir, files[i]);
596 Log.e(LOGTAG, f.getPath() + " delete failed.");
600 } catch (SecurityException e) {
601 // Ignore SecurityExceptions.
605 new Thread(clearCache).start();
609 static void trimCacheIfNeeded() {
610 assert !JniUtil.useChromiumHttpStack();
612 if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) {
613 List<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT);
614 int size = pathList.size();
615 for (int i = 0; i < size; i++) {
616 File f = new File(mBaseDir, pathList.get(i));
618 Log.e(LOGTAG, f.getPath() + " delete failed.");
621 // remove the unreferenced files in the cache directory
622 final List<String> fileList = mDataBase.getAllCacheFileNames();
623 if (fileList == null) return;
624 String[] toDelete = mBaseDir.list(new FilenameFilter() {
625 public boolean accept(File dir, String filename) {
626 if (fileList.contains(filename)) {
633 if (toDelete == null) return;
634 size = toDelete.length;
635 for (int i = 0; i < size; i++) {
636 File f = new File(mBaseDir, toDelete[i]);
638 Log.e(LOGTAG, f.getPath() + " delete failed.");
644 static void clearCache() {
645 assert !JniUtil.useChromiumHttpStack();
648 mDataBase.clearCache();
651 private static boolean isCachableRedirect(int statusCode) {
652 if (statusCode == 301 || statusCode == 302 || statusCode == 307) {
653 // as 303 can't be cached, we do not return true
660 private static String getDatabaseKey(String url, long postIdentifier) {
661 assert !JniUtil.useChromiumHttpStack();
663 if (postIdentifier == 0) return url;
664 return postIdentifier + url;
667 @SuppressWarnings("deprecation")
668 private static void setupFiles(String url, CacheResult cacheRet) {
669 assert !JniUtil.useChromiumHttpStack();
672 // Note: SHA1 is much stronger hash. But the cost of setupFiles() is
673 // 3.2% cpu time for a fresh load of nytimes.com. While a simple
674 // String.hashCode() is only 0.6%. If adding the collision resolving
675 // to String.hashCode(), it makes the cpu time to be 1.6% for a
676 // fresh load, but 5.3% for the worst case where all the files
677 // already exist in the file system, but database is gone. So it
678 // needs to resolve collision for every file at least once.
679 int hashCode = url.hashCode();
680 StringBuffer ret = new StringBuffer(8);
681 appendAsHex(hashCode, ret);
682 String path = ret.toString();
683 File file = new File(mBaseDir, path);
685 boolean checkOldPath = true;
686 // Check hash collision. If the hash file doesn't exist, just
687 // continue. There is a chance that the old cache file is not
688 // same as the hash file. As mDataBase.getCache() is more
689 // expansive than "leak" a file until clear cache, don't bother.
690 // If the hash file exists, make sure that it is same as the
691 // cache file. If it is not, resolve the collision.
692 while (file.exists()) {
694 CacheResult oldResult = mDataBase.getCache(url);
695 if (oldResult != null && oldResult.contentLength > 0) {
696 if (path.equals(oldResult.localPath)) {
697 path = oldResult.localPath;
699 path = oldResult.localPath;
700 file = new File(mBaseDir, path);
704 checkOldPath = false;
706 ret = new StringBuffer(8);
707 appendAsHex(++hashCode, ret);
708 path = ret.toString();
709 file = new File(mBaseDir, path);
712 cacheRet.localPath = path;
713 cacheRet.outFile = file;
715 // get hash in byte[]
716 Digest digest = new SHA1Digest();
717 int digestLen = digest.getDigestSize();
718 byte[] hash = new byte[digestLen];
719 int urlLen = url.length();
720 byte[] data = new byte[urlLen];
721 url.getBytes(0, urlLen, data, 0);
722 digest.update(data, 0, urlLen);
723 digest.doFinal(hash, 0);
724 // convert byte[] to hex String
725 StringBuffer result = new StringBuffer(2 * digestLen);
726 for (int i = 0; i < digestLen; i = i + 4) {
727 int h = (0x00ff & hash[i]) << 24 | (0x00ff & hash[i + 1]) << 16
728 | (0x00ff & hash[i + 2]) << 8 | (0x00ff & hash[i + 3]);
729 appendAsHex(h, result);
731 cacheRet.localPath = result.toString();
732 cacheRet.outFile = new File(mBaseDir, cacheRet.localPath);
736 private static void appendAsHex(int i, StringBuffer ret) {
737 assert !JniUtil.useChromiumHttpStack();
739 String hex = Integer.toHexString(i);
740 switch (hex.length()) {
742 ret.append("0000000");
745 ret.append("000000");
766 private static CacheResult parseHeaders(int statusCode, Headers headers,
768 assert !JniUtil.useChromiumHttpStack();
770 // if the contentLength is already larger than CACHE_MAX_SIZE, skip it
771 if (headers.getContentLength() > CACHE_MAX_SIZE) return null;
773 // The HTML 5 spec, section 6.9.4, step 7.3 of the application cache
774 // process states that HTTP caching rules are ignored for the
775 // purposes of the application cache download process.
776 // At this point we can't tell that if a file is part of this process,
777 // except for the manifest, which has its own mimeType.
778 // TODO: work out a way to distinguish all responses that are part of
779 // the application download process and skip them.
780 if (MANIFEST_MIME.equals(mimeType)) return null;
782 // TODO: if authenticated or secure, return null
783 CacheResult ret = new CacheResult();
784 ret.httpStatusCode = statusCode;
786 ret.location = headers.getLocation();
789 ret.expiresString = headers.getExpires();
790 if (ret.expiresString != null) {
792 ret.expires = AndroidHttpClient.parseDate(ret.expiresString);
793 } catch (IllegalArgumentException ex) {
794 // Take care of the special "-1" and "0" cases
795 if ("-1".equals(ret.expiresString)
796 || "0".equals(ret.expiresString)) {
797 // make it expired, but can be used for history navigation
800 Log.e(LOGTAG, "illegal expires: " + ret.expiresString);
805 ret.contentdisposition = headers.getContentDisposition();
807 ret.crossDomain = headers.getXPermittedCrossDomainPolicies();
809 // lastModified and etag may be set back to http header. So they can't
811 String lastModified = headers.getLastModified();
812 if (lastModified != null && lastModified.length() > 0) {
813 ret.lastModified = lastModified;
816 String etag = headers.getEtag();
817 if (etag != null && etag.length() > 0) {
821 String cacheControl = headers.getCacheControl();
822 if (cacheControl != null) {
823 String[] controls = cacheControl.toLowerCase().split("[ ,;]");
824 for (int i = 0; i < controls.length; i++) {
825 if (NO_STORE.equals(controls[i])) {
828 // According to the spec, 'no-cache' means that the content
829 // must be re-validated on every load. It does not mean that
830 // the content can not be cached. set to expire 0 means it
831 // can only be used in CACHE_MODE_CACHE_ONLY case
832 if (NO_CACHE.equals(controls[i])) {
834 } else if (controls[i].startsWith(MAX_AGE)) {
835 int separator = controls[i].indexOf('=');
837 separator = controls[i].indexOf(':');
840 String s = controls[i].substring(separator + 1);
842 long sec = Long.parseLong(s);
844 ret.expires = System.currentTimeMillis() + 1000
847 } catch (NumberFormatException ex) {
848 if ("1d".equals(s)) {
849 // Take care of the special "1d" case
850 ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
852 Log.e(LOGTAG, "exception in parseHeaders for "
854 + controls[i].substring(separator + 1));
863 // According to RFC 2616 section 14.32:
864 // HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the
865 // client had sent "Cache-Control: no-cache"
866 if (NO_CACHE.equals(headers.getPragma())) {
870 // According to RFC 2616 section 13.2.4, if an expiration has not been
871 // explicitly defined a heuristic to set an expiration may be used.
872 if (ret.expires == -1) {
873 if (ret.httpStatusCode == 301) {
874 // If it is a permanent redirect, and it did not have an
875 // explicit cache directive, then it never expires
876 ret.expires = Long.MAX_VALUE;
877 } else if (ret.httpStatusCode == 302 || ret.httpStatusCode == 307) {
878 // If it is temporary redirect, expires
880 } else if (ret.lastModified == null) {
881 // When we have no last-modified, then expire the content with
882 // in 24hrs as, according to the RFC, longer time requires a
883 // warning 113 to be added to the response.
885 // Only add the default expiration for non-html markup. Some
886 // sites like news.google.com have no cache directives.
887 if (!mimeType.startsWith("text/html")) {
888 ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
890 // Setting a expires as zero will cache the result for
895 // If we have a last-modified value, we could use it to set the
896 // expiration. Suggestion from RFC is 10% of time since
897 // last-modified. As we are on mobile, loads are expensive,
898 // increasing this to 20%.
900 // 24 * 60 * 60 * 1000
901 long lastmod = System.currentTimeMillis() + 86400000;
903 lastmod = AndroidHttpClient.parseDate(ret.lastModified);
904 } catch (IllegalArgumentException ex) {
905 Log.e(LOGTAG, "illegal lastModified: " + ret.lastModified);
907 long difference = System.currentTimeMillis() - lastmod;
908 if (difference > 0) {
909 ret.expires = System.currentTimeMillis() + difference / 5;
911 // last modified is in the future, expire the content
912 // on the last modified
913 ret.expires = lastmod;