• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 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 android.webkit;
18 
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;
24 import java.io.File;
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;
33 import java.util.Map;
34 
35 
36 import org.bouncycastle.crypto.Digest;
37 import org.bouncycastle.crypto.digests.SHA1Digest;
38 
39 /**
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.
47  */
48 public final class CacheManager {
49 
50     private static final String LOGTAG = "cache";
51 
52     static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since";
53     static final String HEADER_KEY_IFNONEMATCH = "if-none-match";
54 
55     private static final String NO_STORE = "no-store";
56     private static final String NO_CACHE = "no-cache";
57     private static final String MAX_AGE = "max-age";
58     private static final String MANIFEST_MIME = "text/cache-manifest";
59 
60     private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
61     private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
62 
63     // Limit the maximum cache file size to half of the normal capacity
64     static long CACHE_MAX_SIZE = (CACHE_THRESHOLD - CACHE_TRIM_AMOUNT) / 2;
65 
66     private static boolean mDisabled;
67 
68     // Reference count the enable/disable transaction
69     private static int mRefCount;
70 
71     // trimCacheIfNeeded() is called when a page is fully loaded. But JavaScript
72     // can load the content, e.g. in a slideshow, continuously, so we need to
73     // trim the cache on a timer base too. endCacheTransaction() is called on a
74     // timer base. We share the same timer with less frequent update.
75     private static int mTrimCacheCount = 0;
76     private static final int TRIM_CACHE_INTERVAL = 5;
77 
78     private static WebViewDatabase mDataBase;
79     private static File mBaseDir;
80 
81     // Flag to clear the cache when the CacheManager is initialized
82     private static boolean mClearCacheOnInit = false;
83 
84     /**
85      * This class represents a resource retrieved from the HTTP cache.
86      * Instances of this class can be obtained by invoking the
87      * CacheManager.getCacheFile() method.
88      */
89     public static class CacheResult {
90         // these fields are saved to the database
91         int httpStatusCode;
92         long contentLength;
93         long expires;
94         String expiresString;
95         String localPath;
96         String lastModified;
97         String etag;
98         String mimeType;
99         String location;
100         String encoding;
101         String contentdisposition;
102         String crossDomain;
103 
104         // these fields are NOT saved to the database
105         InputStream inStream;
106         OutputStream outStream;
107         File outFile;
108 
getHttpStatusCode()109         public int getHttpStatusCode() {
110             return httpStatusCode;
111         }
112 
getContentLength()113         public long getContentLength() {
114             return contentLength;
115         }
116 
getLocalPath()117         public String getLocalPath() {
118             return localPath;
119         }
120 
getExpires()121         public long getExpires() {
122             return expires;
123         }
124 
getExpiresString()125         public String getExpiresString() {
126             return expiresString;
127         }
128 
getLastModified()129         public String getLastModified() {
130             return lastModified;
131         }
132 
getETag()133         public String getETag() {
134             return etag;
135         }
136 
getMimeType()137         public String getMimeType() {
138             return mimeType;
139         }
140 
getLocation()141         public String getLocation() {
142             return location;
143         }
144 
getEncoding()145         public String getEncoding() {
146             return encoding;
147         }
148 
getContentDisposition()149         public String getContentDisposition() {
150             return contentdisposition;
151         }
152 
153         // For out-of-package access to the underlying streams.
getInputStream()154         public InputStream getInputStream() {
155             return inStream;
156         }
157 
getOutputStream()158         public OutputStream getOutputStream() {
159             return outStream;
160         }
161 
162         // These fields can be set manually.
setInputStream(InputStream stream)163         public void setInputStream(InputStream stream) {
164             this.inStream = stream;
165         }
166 
setEncoding(String encoding)167         public void setEncoding(String encoding) {
168             this.encoding = encoding;
169         }
170     }
171 
172     /**
173      * initialize the CacheManager. WebView should handle this for each process.
174      *
175      * @param context The application context.
176      */
init(Context context)177     static void init(Context context) {
178         mDataBase = WebViewDatabase.getInstance(context.getApplicationContext());
179         mBaseDir = new File(context.getCacheDir(), "webviewCache");
180         if (createCacheDirectory() && mClearCacheOnInit) {
181             removeAllCacheFiles();
182             mClearCacheOnInit = false;
183         }
184     }
185 
186     /**
187      * Create the cache directory if it does not already exist.
188      *
189      * @return true if the cache directory didn't exist and was created.
190      */
createCacheDirectory()191     static private boolean createCacheDirectory() {
192         if (!mBaseDir.exists()) {
193             if(!mBaseDir.mkdirs()) {
194                 Log.w(LOGTAG, "Unable to create webviewCache directory");
195                 return false;
196             }
197             FileUtils.setPermissions(
198                     mBaseDir.toString(),
199                     FileUtils.S_IRWXU|FileUtils.S_IRWXG|FileUtils.S_IXOTH,
200                     -1, -1);
201             // If we did create the directory, we need to flush
202             // the cache database. The directory could be recreated
203             // because the system flushed all the data/cache directories
204             // to free up disk space.
205             // delete rows in the cache database
206             WebViewWorker.getHandler().sendEmptyMessage(
207                     WebViewWorker.MSG_CLEAR_CACHE);
208             return true;
209         }
210         return false;
211     }
212 
213     /**
214      * get the base directory of the cache. With localPath of the CacheResult,
215      * it identifies the cache file.
216      *
217      * @return File The base directory of the cache.
218      */
getCacheFileBaseDir()219     public static File getCacheFileBaseDir() {
220         return mBaseDir;
221     }
222 
223     /**
224      * set the flag to control whether cache is enabled or disabled
225      *
226      * @param disabled true to disable the cache
227      */
setCacheDisabled(boolean disabled)228     static void setCacheDisabled(boolean disabled) {
229         if (disabled == mDisabled) {
230             return;
231         }
232         mDisabled = disabled;
233         if (mDisabled) {
234             removeAllCacheFiles();
235         }
236     }
237 
238     /**
239      * get the state of the current cache, enabled or disabled
240      *
241      * @return return if it is disabled
242      */
cacheDisabled()243     public static boolean cacheDisabled() {
244         return mDisabled;
245     }
246 
247     // only called from WebViewWorkerThread
248     // make sure to call enableTransaction/disableTransaction in pair
enableTransaction()249     static boolean enableTransaction() {
250         if (++mRefCount == 1) {
251             mDataBase.startCacheTransaction();
252             return true;
253         }
254         return false;
255     }
256 
257     // only called from WebViewWorkerThread
258     // make sure to call enableTransaction/disableTransaction in pair
disableTransaction()259     static boolean disableTransaction() {
260         if (--mRefCount == 0) {
261             mDataBase.endCacheTransaction();
262             return true;
263         }
264         return false;
265     }
266 
267     // only called from WebViewWorkerThread
268     // make sure to call startTransaction/endTransaction in pair
startTransaction()269     static boolean startTransaction() {
270         return mDataBase.startCacheTransaction();
271     }
272 
273     // only called from WebViewWorkerThread
274     // make sure to call startTransaction/endTransaction in pair
endTransaction()275     static boolean endTransaction() {
276         boolean ret = mDataBase.endCacheTransaction();
277         if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) {
278             mTrimCacheCount = 0;
279             trimCacheIfNeeded();
280         }
281         return ret;
282     }
283 
284     // only called from WebCore Thread
285     // make sure to call startCacheTransaction/endCacheTransaction in pair
286     /**
287      * @deprecated Always returns false.
288      */
289     @Deprecated
startCacheTransaction()290     public static boolean startCacheTransaction() {
291         return false;
292     }
293 
294     // only called from WebCore Thread
295     // make sure to call startCacheTransaction/endCacheTransaction in pair
296     /**
297      * @deprecated Always returns false.
298      */
299     @Deprecated
endCacheTransaction()300     public static boolean endCacheTransaction() {
301         return false;
302     }
303 
304     /**
305      * Given a url, returns the CacheResult if exists. Otherwise returns null.
306      * If headers are provided and a cache needs validation,
307      * HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE will be set in the
308      * cached headers.
309      *
310      * @return the CacheResult for a given url
311      */
getCacheFile(String url, Map<String, String> headers)312     public static CacheResult getCacheFile(String url,
313             Map<String, String> headers) {
314         return getCacheFile(url, 0, headers);
315     }
316 
getCacheFile(String url, long postIdentifier, Map<String, String> headers)317     static CacheResult getCacheFile(String url, long postIdentifier,
318             Map<String, String> headers) {
319         if (mDisabled) {
320             return null;
321         }
322 
323         String databaseKey = getDatabaseKey(url, postIdentifier);
324 
325         CacheResult result = mDataBase.getCache(databaseKey);
326         if (result != null) {
327             if (result.contentLength == 0) {
328                 if (!checkCacheRedirect(result.httpStatusCode)) {
329                     // this should not happen. If it does, remove it.
330                     mDataBase.removeCache(databaseKey);
331                     return null;
332                 }
333             } else {
334                 File src = new File(mBaseDir, result.localPath);
335                 try {
336                     // open here so that even the file is deleted, the content
337                     // is still readable by the caller until close() is called
338                     result.inStream = new FileInputStream(src);
339                 } catch (FileNotFoundException e) {
340                     // the files in the cache directory can be removed by the
341                     // system. If it is gone, clean up the database
342                     mDataBase.removeCache(databaseKey);
343                     return null;
344                 }
345             }
346         } else {
347             return null;
348         }
349 
350         // null headers request coming from CACHE_MODE_CACHE_ONLY
351         // which implies that it needs cache even it is expired.
352         // negative expires means time in the far future.
353         if (headers != null && result.expires >= 0
354                 && result.expires <= System.currentTimeMillis()) {
355             if (result.lastModified == null && result.etag == null) {
356                 return null;
357             }
358             // return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE
359             // for requesting validation
360             if (result.etag != null) {
361                 headers.put(HEADER_KEY_IFNONEMATCH, result.etag);
362             }
363             if (result.lastModified != null) {
364                 headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified);
365             }
366         }
367 
368         if (DebugFlags.CACHE_MANAGER) {
369             Log.v(LOGTAG, "getCacheFile for url " + url);
370         }
371 
372         return result;
373     }
374 
375     /**
376      * Given a url and its full headers, returns CacheResult if a local cache
377      * can be stored. Otherwise returns null. The mimetype is passed in so that
378      * the function can use the mimetype that will be passed to WebCore which
379      * could be different from the mimetype defined in the headers.
380      * forceCache is for out-of-package callers to force creation of a
381      * CacheResult, and is used to supply surrogate responses for URL
382      * interception.
383      * @return CacheResult for a given url
384      * @hide - hide createCacheFile since it has a parameter of type headers, which is
385      * in a hidden package.
386      */
createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache)387     public static CacheResult createCacheFile(String url, int statusCode,
388             Headers headers, String mimeType, boolean forceCache) {
389         return createCacheFile(url, statusCode, headers, mimeType, 0,
390                 forceCache);
391     }
392 
createCacheFile(String url, int statusCode, Headers headers, String mimeType, long postIdentifier, boolean forceCache)393     static CacheResult createCacheFile(String url, int statusCode,
394             Headers headers, String mimeType, long postIdentifier,
395             boolean forceCache) {
396         if (!forceCache && mDisabled) {
397             return null;
398         }
399 
400         String databaseKey = getDatabaseKey(url, postIdentifier);
401 
402         // according to the rfc 2616, the 303 response MUST NOT be cached.
403         if (statusCode == 303) {
404             // remove the saved cache if there is any
405             mDataBase.removeCache(databaseKey);
406             return null;
407         }
408 
409         // like the other browsers, do not cache redirects containing a cookie
410         // header.
411         if (checkCacheRedirect(statusCode) && !headers.getSetCookie().isEmpty()) {
412             // remove the saved cache if there is any
413             mDataBase.removeCache(databaseKey);
414             return null;
415         }
416 
417         CacheResult ret = parseHeaders(statusCode, headers, mimeType);
418         if (ret == null) {
419             // this should only happen if the headers has "no-store" in the
420             // cache-control. remove the saved cache if there is any
421             mDataBase.removeCache(databaseKey);
422         } else {
423             setupFiles(databaseKey, ret);
424             try {
425                 ret.outStream = new FileOutputStream(ret.outFile);
426             } catch (FileNotFoundException e) {
427                 // This can happen with the system did a purge and our
428                 // subdirectory has gone, so lets try to create it again
429                 if (createCacheDirectory()) {
430                     try {
431                         ret.outStream = new FileOutputStream(ret.outFile);
432                     } catch  (FileNotFoundException e2) {
433                         // We failed to create the file again, so there
434                         // is something else wrong. Return null.
435                         return null;
436                     }
437                 } else {
438                     // Failed to create cache directory
439                     return null;
440                 }
441             }
442             ret.mimeType = mimeType;
443         }
444 
445         return ret;
446     }
447 
448     /**
449      * Save the info of a cache file for a given url to the CacheMap so that it
450      * can be reused later
451      */
saveCacheFile(String url, CacheResult cacheRet)452     public static void saveCacheFile(String url, CacheResult cacheRet) {
453         saveCacheFile(url, 0, cacheRet);
454     }
455 
saveCacheFile(String url, long postIdentifier, CacheResult cacheRet)456     static void saveCacheFile(String url, long postIdentifier,
457             CacheResult cacheRet) {
458         try {
459             cacheRet.outStream.close();
460         } catch (IOException e) {
461             return;
462         }
463 
464         if (!cacheRet.outFile.exists()) {
465             // the file in the cache directory can be removed by the system
466             return;
467         }
468 
469         boolean redirect = checkCacheRedirect(cacheRet.httpStatusCode);
470         if (redirect) {
471             // location is in database, no need to keep the file
472             cacheRet.contentLength = 0;
473             cacheRet.localPath = "";
474         }
475         if ((redirect || cacheRet.contentLength == 0)
476                 && !cacheRet.outFile.delete()) {
477             Log.e(LOGTAG, cacheRet.outFile.getPath() + " delete failed.");
478         }
479         if (cacheRet.contentLength == 0) {
480             return;
481         }
482 
483         mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet);
484 
485         if (DebugFlags.CACHE_MANAGER) {
486             Log.v(LOGTAG, "saveCacheFile for url " + url);
487         }
488     }
489 
cleanupCacheFile(CacheResult cacheRet)490     static boolean cleanupCacheFile(CacheResult cacheRet) {
491         try {
492             cacheRet.outStream.close();
493         } catch (IOException e) {
494             return false;
495         }
496         return cacheRet.outFile.delete();
497     }
498 
499     /**
500      * remove all cache files
501      *
502      * @return true if it succeeds
503      */
removeAllCacheFiles()504     static boolean removeAllCacheFiles() {
505         // Note, this is called before init() when the database is
506         // created or upgraded.
507         if (mBaseDir == null) {
508             // Init() has not been called yet, so just flag that
509             // we need to clear the cache when init() is called.
510             mClearCacheOnInit = true;
511             return true;
512         }
513         // delete rows in the cache database
514         WebViewWorker.getHandler().sendEmptyMessage(
515                 WebViewWorker.MSG_CLEAR_CACHE);
516         // delete cache files in a separate thread to not block UI.
517         final Runnable clearCache = new Runnable() {
518             public void run() {
519                 // delete all cache files
520                 try {
521                     String[] files = mBaseDir.list();
522                     // if mBaseDir doesn't exist, files can be null.
523                     if (files != null) {
524                         for (int i = 0; i < files.length; i++) {
525                             File f = new File(mBaseDir, files[i]);
526                             if (!f.delete()) {
527                                 Log.e(LOGTAG, f.getPath() + " delete failed.");
528                             }
529                         }
530                     }
531                 } catch (SecurityException e) {
532                     // Ignore SecurityExceptions.
533                 }
534             }
535         };
536         new Thread(clearCache).start();
537         return true;
538     }
539 
540     /**
541      * Return true if the cache is empty.
542      */
cacheEmpty()543     static boolean cacheEmpty() {
544         return mDataBase.hasCache();
545     }
546 
trimCacheIfNeeded()547     static void trimCacheIfNeeded() {
548         if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) {
549             List<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT);
550             int size = pathList.size();
551             for (int i = 0; i < size; i++) {
552                 File f = new File(mBaseDir, pathList.get(i));
553                 if (!f.delete()) {
554                     Log.e(LOGTAG, f.getPath() + " delete failed.");
555                 }
556             }
557             // remove the unreferenced files in the cache directory
558             final List<String> fileList = mDataBase.getAllCacheFileNames();
559             if (fileList == null) return;
560             String[] toDelete = mBaseDir.list(new FilenameFilter() {
561                 public boolean accept(File dir, String filename) {
562                     if (fileList.contains(filename)) {
563                         return false;
564                     } else {
565                         return true;
566                     }
567                 }
568             });
569             if (toDelete == null) return;
570             size = toDelete.length;
571             for (int i = 0; i < size; i++) {
572                 File f = new File(mBaseDir, toDelete[i]);
573                 if (!f.delete()) {
574                     Log.e(LOGTAG, f.getPath() + " delete failed.");
575                 }
576             }
577         }
578     }
579 
clearCache()580     static void clearCache() {
581         // delete database
582         mDataBase.clearCache();
583     }
584 
checkCacheRedirect(int statusCode)585     private static boolean checkCacheRedirect(int statusCode) {
586         if (statusCode == 301 || statusCode == 302 || statusCode == 307) {
587             // as 303 can't be cached, we do not return true
588             return true;
589         } else {
590             return false;
591         }
592     }
593 
getDatabaseKey(String url, long postIdentifier)594     private static String getDatabaseKey(String url, long postIdentifier) {
595         if (postIdentifier == 0) return url;
596         return postIdentifier + url;
597     }
598 
599     @SuppressWarnings("deprecation")
setupFiles(String url, CacheResult cacheRet)600     private static void setupFiles(String url, CacheResult cacheRet) {
601         if (true) {
602             // Note: SHA1 is much stronger hash. But the cost of setupFiles() is
603             // 3.2% cpu time for a fresh load of nytimes.com. While a simple
604             // String.hashCode() is only 0.6%. If adding the collision resolving
605             // to String.hashCode(), it makes the cpu time to be 1.6% for a
606             // fresh load, but 5.3% for the worst case where all the files
607             // already exist in the file system, but database is gone. So it
608             // needs to resolve collision for every file at least once.
609             int hashCode = url.hashCode();
610             StringBuffer ret = new StringBuffer(8);
611             appendAsHex(hashCode, ret);
612             String path = ret.toString();
613             File file = new File(mBaseDir, path);
614             if (true) {
615                 boolean checkOldPath = true;
616                 // Check hash collision. If the hash file doesn't exist, just
617                 // continue. There is a chance that the old cache file is not
618                 // same as the hash file. As mDataBase.getCache() is more
619                 // expansive than "leak" a file until clear cache, don't bother.
620                 // If the hash file exists, make sure that it is same as the
621                 // cache file. If it is not, resolve the collision.
622                 while (file.exists()) {
623                     if (checkOldPath) {
624                         CacheResult oldResult = mDataBase.getCache(url);
625                         if (oldResult != null && oldResult.contentLength > 0) {
626                             if (path.equals(oldResult.localPath)) {
627                                 path = oldResult.localPath;
628                             } else {
629                                 path = oldResult.localPath;
630                                 file = new File(mBaseDir, path);
631                             }
632                             break;
633                         }
634                         checkOldPath = false;
635                     }
636                     ret = new StringBuffer(8);
637                     appendAsHex(++hashCode, ret);
638                     path = ret.toString();
639                     file = new File(mBaseDir, path);
640                 }
641             }
642             cacheRet.localPath = path;
643             cacheRet.outFile = file;
644         } else {
645             // get hash in byte[]
646             Digest digest = new SHA1Digest();
647             int digestLen = digest.getDigestSize();
648             byte[] hash = new byte[digestLen];
649             int urlLen = url.length();
650             byte[] data = new byte[urlLen];
651             url.getBytes(0, urlLen, data, 0);
652             digest.update(data, 0, urlLen);
653             digest.doFinal(hash, 0);
654             // convert byte[] to hex String
655             StringBuffer result = new StringBuffer(2 * digestLen);
656             for (int i = 0; i < digestLen; i = i + 4) {
657                 int h = (0x00ff & hash[i]) << 24 | (0x00ff & hash[i + 1]) << 16
658                         | (0x00ff & hash[i + 2]) << 8 | (0x00ff & hash[i + 3]);
659                 appendAsHex(h, result);
660             }
661             cacheRet.localPath = result.toString();
662             cacheRet.outFile = new File(mBaseDir, cacheRet.localPath);
663         }
664     }
665 
appendAsHex(int i, StringBuffer ret)666     private static void appendAsHex(int i, StringBuffer ret) {
667         String hex = Integer.toHexString(i);
668         switch (hex.length()) {
669             case 1:
670                 ret.append("0000000");
671                 break;
672             case 2:
673                 ret.append("000000");
674                 break;
675             case 3:
676                 ret.append("00000");
677                 break;
678             case 4:
679                 ret.append("0000");
680                 break;
681             case 5:
682                 ret.append("000");
683                 break;
684             case 6:
685                 ret.append("00");
686                 break;
687             case 7:
688                 ret.append("0");
689                 break;
690         }
691         ret.append(hex);
692     }
693 
parseHeaders(int statusCode, Headers headers, String mimeType)694     private static CacheResult parseHeaders(int statusCode, Headers headers,
695             String mimeType) {
696         // if the contentLength is already larger than CACHE_MAX_SIZE, skip it
697         if (headers.getContentLength() > CACHE_MAX_SIZE) return null;
698 
699         // The HTML 5 spec, section 6.9.4, step 7.3 of the application cache
700         // process states that HTTP caching rules are ignored for the
701         // purposes of the application cache download process.
702         // At this point we can't tell that if a file is part of this process,
703         // except for the manifest, which has its own mimeType.
704         // TODO: work out a way to distinguish all responses that are part of
705         // the application download process and skip them.
706         if (MANIFEST_MIME.equals(mimeType)) return null;
707 
708         // TODO: if authenticated or secure, return null
709         CacheResult ret = new CacheResult();
710         ret.httpStatusCode = statusCode;
711 
712         String location = headers.getLocation();
713         if (location != null) ret.location = location;
714 
715         ret.expires = -1;
716         ret.expiresString = headers.getExpires();
717         if (ret.expiresString != null) {
718             try {
719                 ret.expires = AndroidHttpClient.parseDate(ret.expiresString);
720             } catch (IllegalArgumentException ex) {
721                 // Take care of the special "-1" and "0" cases
722                 if ("-1".equals(ret.expiresString)
723                         || "0".equals(ret.expiresString)) {
724                     // make it expired, but can be used for history navigation
725                     ret.expires = 0;
726                 } else {
727                     Log.e(LOGTAG, "illegal expires: " + ret.expiresString);
728                 }
729             }
730         }
731 
732         String contentDisposition = headers.getContentDisposition();
733         if (contentDisposition != null) {
734             ret.contentdisposition = contentDisposition;
735         }
736 
737         String crossDomain = headers.getXPermittedCrossDomainPolicies();
738         if (crossDomain != null) {
739             ret.crossDomain = crossDomain;
740         }
741 
742         // lastModified and etag may be set back to http header. So they can't
743         // be empty string.
744         String lastModified = headers.getLastModified();
745         if (lastModified != null && lastModified.length() > 0) {
746             ret.lastModified = lastModified;
747         }
748 
749         String etag = headers.getEtag();
750         if (etag != null && etag.length() > 0) ret.etag = etag;
751 
752         String cacheControl = headers.getCacheControl();
753         if (cacheControl != null) {
754             String[] controls = cacheControl.toLowerCase().split("[ ,;]");
755             for (int i = 0; i < controls.length; i++) {
756                 if (NO_STORE.equals(controls[i])) {
757                     return null;
758                 }
759                 // According to the spec, 'no-cache' means that the content
760                 // must be re-validated on every load. It does not mean that
761                 // the content can not be cached. set to expire 0 means it
762                 // can only be used in CACHE_MODE_CACHE_ONLY case
763                 if (NO_CACHE.equals(controls[i])) {
764                     ret.expires = 0;
765                 } else if (controls[i].startsWith(MAX_AGE)) {
766                     int separator = controls[i].indexOf('=');
767                     if (separator < 0) {
768                         separator = controls[i].indexOf(':');
769                     }
770                     if (separator > 0) {
771                         String s = controls[i].substring(separator + 1);
772                         try {
773                             long sec = Long.parseLong(s);
774                             if (sec >= 0) {
775                                 ret.expires = System.currentTimeMillis() + 1000
776                                         * sec;
777                             }
778                         } catch (NumberFormatException ex) {
779                             if ("1d".equals(s)) {
780                                 // Take care of the special "1d" case
781                                 ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
782                             } else {
783                                 Log.e(LOGTAG, "exception in parseHeaders for "
784                                         + "max-age:"
785                                         + controls[i].substring(separator + 1));
786                                 ret.expires = 0;
787                             }
788                         }
789                     }
790                 }
791             }
792         }
793 
794         // According to RFC 2616 section 14.32:
795         // HTTP/1.1 caches SHOULD treat "Pragma: no-cache" as if the
796         // client had sent "Cache-Control: no-cache"
797         if (NO_CACHE.equals(headers.getPragma())) {
798             ret.expires = 0;
799         }
800 
801         // According to RFC 2616 section 13.2.4, if an expiration has not been
802         // explicitly defined a heuristic to set an expiration may be used.
803         if (ret.expires == -1) {
804             if (ret.httpStatusCode == 301) {
805                 // If it is a permanent redirect, and it did not have an
806                 // explicit cache directive, then it never expires
807                 ret.expires = Long.MAX_VALUE;
808             } else if (ret.httpStatusCode == 302 || ret.httpStatusCode == 307) {
809                 // If it is temporary redirect, expires
810                 ret.expires = 0;
811             } else if (ret.lastModified == null) {
812                 // When we have no last-modified, then expire the content with
813                 // in 24hrs as, according to the RFC, longer time requires a
814                 // warning 113 to be added to the response.
815 
816                 // Only add the default expiration for non-html markup. Some
817                 // sites like news.google.com have no cache directives.
818                 if (!mimeType.startsWith("text/html")) {
819                     ret.expires = System.currentTimeMillis() + 86400000; // 24*60*60*1000
820                 } else {
821                     // Setting a expires as zero will cache the result for
822                     // forward/back nav.
823                     ret.expires = 0;
824                 }
825             } else {
826                 // If we have a last-modified value, we could use it to set the
827                 // expiration. Suggestion from RFC is 10% of time since
828                 // last-modified. As we are on mobile, loads are expensive,
829                 // increasing this to 20%.
830 
831                 // 24 * 60 * 60 * 1000
832                 long lastmod = System.currentTimeMillis() + 86400000;
833                 try {
834                     lastmod = AndroidHttpClient.parseDate(ret.lastModified);
835                 } catch (IllegalArgumentException ex) {
836                     Log.e(LOGTAG, "illegal lastModified: " + ret.lastModified);
837                 }
838                 long difference = System.currentTimeMillis() - lastmod;
839                 if (difference > 0) {
840                     ret.expires = System.currentTimeMillis() + difference / 5;
841                 } else {
842                     // last modified is in the future, expire the content
843                     // on the last modified
844                     ret.expires = lastmod;
845                 }
846             }
847         }
848 
849         return ret;
850     }
851 }
852