OSDN Git Service

Avoid superfluous calls to CacheManager with the Chromium HTTP stack
[android-x86/frameworks-base.git] / core / java / android / webkit / CacheManager.java
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  * @deprecated Access to the HTTP cache will be removed in a future release.
49  */
50 @Deprecated
51 public final class CacheManager {
52
53     private static final String LOGTAG = "cache";
54
55     static final String HEADER_KEY_IFMODIFIEDSINCE = "if-modified-since";
56     static final String HEADER_KEY_IFNONEMATCH = "if-none-match";
57
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";
62
63     private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
64     private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
65
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;
68
69     private static boolean mDisabled;
70
71     // Reference count the enable/disable transaction
72     private static int mRefCount;
73
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;
80
81     private static WebViewDatabase mDataBase;
82     private static File mBaseDir;
83     
84     // Flag to clear the cache when the CacheManager is initialized
85     private static boolean mClearCacheOnInit = false;
86
87     /**
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.
91      *
92      * @deprecated Access to the HTTP cache will be removed in a future release.
93      */
94     @Deprecated
95     public static class CacheResult {
96         // these fields are saved to the database
97         int httpStatusCode;
98         long contentLength;
99         long expires;
100         String expiresString;
101         String localPath;
102         String lastModified;
103         String etag;
104         String mimeType;
105         String location;
106         String encoding;
107         String contentdisposition;
108         String crossDomain;
109
110         // these fields are NOT saved to the database
111         InputStream inStream;
112         OutputStream outStream;
113         File outFile;
114
115         public int getHttpStatusCode() {
116             return httpStatusCode;
117         }
118
119         public long getContentLength() {
120             return contentLength;
121         }
122
123         public String getLocalPath() {
124             return localPath;
125         }
126
127         public long getExpires() {
128             return expires;
129         }
130
131         public String getExpiresString() {
132             return expiresString;
133         }
134
135         public String getLastModified() {
136             return lastModified;
137         }
138
139         public String getETag() {
140             return etag;
141         }
142
143         public String getMimeType() {
144             return mimeType;
145         }
146
147         public String getLocation() {
148             return location;
149         }
150
151         public String getEncoding() {
152             return encoding;
153         }
154
155         public String getContentDisposition() {
156             return contentdisposition;
157         }
158
159         // For out-of-package access to the underlying streams.
160         public InputStream getInputStream() {
161             return inStream;
162         }
163
164         public OutputStream getOutputStream() {
165             return outStream;
166         }
167
168         // These fields can be set manually.
169         public void setInputStream(InputStream stream) {
170             this.inStream = stream;
171         }
172
173         public void setEncoding(String encoding) {
174             this.encoding = encoding;
175         }
176
177         /**
178          * @hide
179          */
180         public void setContentLength(long contentLength) {
181             this.contentLength = contentLength;
182         }
183     }
184
185     /**
186      * Initialize the CacheManager.
187      *
188      * Note that this is called automatically when a {@link android.webkit.WebView} is created.
189      *
190      * @param context The application context.
191      */
192     static void init(Context context) {
193         if (JniUtil.useChromiumHttpStack()) {
194             // TODO: Need to init mBaseDir.
195             return;
196         }
197
198         mDataBase = WebViewDatabase.getInstance(context.getApplicationContext());
199         mBaseDir = new File(context.getCacheDir(), "webviewCache");
200         if (createCacheDirectory() && mClearCacheOnInit) {
201             removeAllCacheFiles();
202             mClearCacheOnInit = false;
203         }
204     }
205
206     /**
207      * Create the cache directory if it does not already exist.
208      *
209      * @return true if the cache directory didn't exist and was created.
210      */
211     static private boolean createCacheDirectory() {
212         assert !JniUtil.useChromiumHttpStack();
213
214         if (!mBaseDir.exists()) {
215             if(!mBaseDir.mkdirs()) {
216                 Log.w(LOGTAG, "Unable to create webviewCache directory");
217                 return false;
218             }
219             FileUtils.setPermissions(
220                     mBaseDir.toString(),
221                     FileUtils.S_IRWXU | FileUtils.S_IRWXG,
222                     -1, -1);
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);
230             return true;
231         }
232         return false;
233     }
234
235     /**
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.
239      *
240      * @return File The base directory of the cache.
241      *
242      * @deprecated Access to the HTTP cache will be removed in a future release.
243      */
244     @Deprecated
245     public static File getCacheFileBaseDir() {
246         return mBaseDir;
247     }
248
249     /**
250      * Sets whether the cache is disabled.
251      *
252      * @param disabled Whether the cache should be disabled
253      */
254     static void setCacheDisabled(boolean disabled) {
255         assert !JniUtil.useChromiumHttpStack();
256
257         if (disabled == mDisabled) {
258             return;
259         }
260         mDisabled = disabled;
261         if (mDisabled) {
262             removeAllCacheFiles();
263         }
264     }
265
266     /**
267      * Whether the cache is disabled.
268      *
269      * @return return Whether the cache is disabled
270      *
271      * @deprecated Access to the HTTP cache will be removed in a future release.
272      */
273     @Deprecated
274     public static boolean cacheDisabled() {
275         return mDisabled;
276     }
277
278     // only called from WebViewWorkerThread
279     // make sure to call enableTransaction/disableTransaction in pair
280     static boolean enableTransaction() {
281         assert !JniUtil.useChromiumHttpStack();
282
283         if (++mRefCount == 1) {
284             mDataBase.startCacheTransaction();
285             return true;
286         }
287         return false;
288     }
289
290     // only called from WebViewWorkerThread
291     // make sure to call enableTransaction/disableTransaction in pair
292     static boolean disableTransaction() {
293         assert !JniUtil.useChromiumHttpStack();
294
295         if (--mRefCount == 0) {
296             mDataBase.endCacheTransaction();
297             return true;
298         }
299         return false;
300     }
301
302     // only called from WebViewWorkerThread
303     // make sure to call startTransaction/endTransaction in pair
304     static boolean startTransaction() {
305         assert !JniUtil.useChromiumHttpStack();
306
307         return mDataBase.startCacheTransaction();
308     }
309
310     // only called from WebViewWorkerThread
311     // make sure to call startTransaction/endTransaction in pair
312     static boolean endTransaction() {
313         assert !JniUtil.useChromiumHttpStack();
314
315         boolean ret = mDataBase.endCacheTransaction();
316         if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) {
317             mTrimCacheCount = 0;
318             trimCacheIfNeeded();
319         }
320         return ret;
321     }
322
323     // only called from WebCore Thread
324     // make sure to call startCacheTransaction/endCacheTransaction in pair
325     /**
326      * @deprecated Always returns false.
327      */
328     @Deprecated
329     public static boolean startCacheTransaction() {
330         return false;
331     }
332
333     // only called from WebCore Thread
334     // make sure to call startCacheTransaction/endCacheTransaction in pair
335     /**
336      * @deprecated Always returns false.
337      */
338     @Deprecated
339     public static boolean endCacheTransaction() {
340         return false;
341     }
342
343     /**
344      * Given a URL, returns the corresponding CacheResult if it exists, or null otherwise.
345      *
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.
350      *
351      * @return The CacheResult for the given URL
352      *
353      * @deprecated Access to the HTTP cache will be removed in a future release.
354      */
355     @Deprecated
356     public static CacheResult getCacheFile(String url,
357             Map<String, String> headers) {
358         return getCacheFile(url, 0, headers);
359     }
360
361     static CacheResult getCacheFile(String url, long postIdentifier,
362             Map<String, String> headers) {
363         if (mDisabled) {
364             return null;
365         }
366
367         if (JniUtil.useChromiumHttpStack()) {
368             // TODO: Implement this.
369             return null;
370         }
371
372         String databaseKey = getDatabaseKey(url, postIdentifier);
373         CacheResult result = mDataBase.getCache(databaseKey);
374         if (result == null) {
375             return null;
376         }
377         if (result.contentLength == 0) {
378             if (!isCachableRedirect(result.httpStatusCode)) {
379                 // This should not happen. If it does, remove it.
380                 mDataBase.removeCache(databaseKey);
381                 return null;
382             }
383         } else {
384             File src = new File(mBaseDir, result.localPath);
385             try {
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);
393                 return null;
394             }
395         }
396
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) {
403                 return null;
404             }
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);
409             }
410             if (result.lastModified != null) {
411                 headers.put(HEADER_KEY_IFMODIFIEDSINCE, result.lastModified);
412             }
413         }
414
415         if (DebugFlags.CACHE_MANAGER) {
416             Log.v(LOGTAG, "getCacheFile for url " + url);
417         }
418
419         return result;
420     }
421
422     /**
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
429      * interception.
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.
433      *
434      * @deprecated Access to the HTTP cache will be removed in a future release.
435      */
436     @Deprecated
437     public static CacheResult createCacheFile(String url, int statusCode,
438             Headers headers, String mimeType, boolean forceCache) {
439         if (JniUtil.useChromiumHttpStack()) {
440             // TODO: Implement this.
441             return null;
442         }
443
444         return createCacheFile(url, statusCode, headers, mimeType, 0,
445                 forceCache);
446     }
447
448     static CacheResult createCacheFile(String url, int statusCode,
449             Headers headers, String mimeType, long postIdentifier,
450             boolean forceCache) {
451         assert !JniUtil.useChromiumHttpStack();
452
453         if (!forceCache && mDisabled) {
454             return null;
455         }
456
457         String databaseKey = getDatabaseKey(url, postIdentifier);
458
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);
463             return null;
464         }
465
466         // like the other browsers, do not cache redirects containing a cookie
467         // header.
468         if (isCachableRedirect(statusCode) && !headers.getSetCookie().isEmpty()) {
469             // remove the saved cache if there is any
470             mDataBase.removeCache(databaseKey);
471             return null;
472         }
473
474         CacheResult ret = parseHeaders(statusCode, headers, mimeType);
475         if (ret == null) {
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);
479         } else {
480             setupFiles(databaseKey, ret);
481             try {
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()) {
487                     try {
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.
492                         return null;
493                     }
494                 } else {
495                     // Failed to create cache directory
496                     return null;
497                 }
498             }
499             ret.mimeType = mimeType;
500         }
501
502         return ret;
503     }
504
505     /**
506      * Save the info of a cache file for a given url to the CacheMap so that it
507      * can be reused later
508      *
509      * @deprecated Access to the HTTP cache will be removed in a future release.
510      */
511     @Deprecated
512     public static void saveCacheFile(String url, CacheResult cacheRet) {
513         saveCacheFile(url, 0, cacheRet);
514     }
515
516     static void saveCacheFile(String url, long postIdentifier,
517             CacheResult cacheRet) {
518         try {
519             cacheRet.outStream.close();
520         } catch (IOException e) {
521             return;
522         }
523
524         if (JniUtil.useChromiumHttpStack()) {
525             // TODO: Implement this.
526             return;
527         }
528
529         if (!cacheRet.outFile.exists()) {
530             // the file in the cache directory can be removed by the system
531             return;
532         }
533
534         boolean redirect = isCachableRedirect(cacheRet.httpStatusCode);
535         if (redirect) {
536             // location is in database, no need to keep the file
537             cacheRet.contentLength = 0;
538             cacheRet.localPath = "";
539         }
540         if ((redirect || cacheRet.contentLength == 0)
541                 && !cacheRet.outFile.delete()) {
542             Log.e(LOGTAG, cacheRet.outFile.getPath() + " delete failed.");
543         }
544         if (cacheRet.contentLength == 0) {
545             return;
546         }
547
548         mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet);
549
550         if (DebugFlags.CACHE_MANAGER) {
551             Log.v(LOGTAG, "saveCacheFile for url " + url);
552         }
553     }
554
555     static boolean cleanupCacheFile(CacheResult cacheRet) {
556         assert !JniUtil.useChromiumHttpStack();
557
558         try {
559             cacheRet.outStream.close();
560         } catch (IOException e) {
561             return false;
562         }
563         return cacheRet.outFile.delete();
564     }
565
566     /**
567      * Remove all cache files.
568      *
569      * @return Whether the removal succeeded.
570      */
571     static boolean removeAllCacheFiles() {
572         assert !JniUtil.useChromiumHttpStack();
573
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;
580             return true;
581         }
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() {
587             public void run() {
588                 // delete all cache files
589                 try {
590                     String[] files = mBaseDir.list();
591                     // if mBaseDir doesn't exist, files can be null.
592                     if (files != null) {
593                         for (int i = 0; i < files.length; i++) {
594                             File f = new File(mBaseDir, files[i]);
595                             if (!f.delete()) {
596                                 Log.e(LOGTAG, f.getPath() + " delete failed.");
597                             }
598                         }
599                     }
600                 } catch (SecurityException e) {
601                     // Ignore SecurityExceptions.
602                 }
603             }
604         };
605         new Thread(clearCache).start();
606         return true;
607     }
608
609     static void trimCacheIfNeeded() {
610         assert !JniUtil.useChromiumHttpStack();
611
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));
617                 if (!f.delete()) {
618                     Log.e(LOGTAG, f.getPath() + " delete failed.");
619                 }
620             }
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)) {
627                         return false;
628                     } else {
629                         return true;
630                     }
631                 }
632             });
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]);
637                 if (!f.delete()) {
638                     Log.e(LOGTAG, f.getPath() + " delete failed.");
639                 }
640             }
641         }
642     }
643
644     static void clearCache() {
645         assert !JniUtil.useChromiumHttpStack();
646
647         // delete database
648         mDataBase.clearCache();
649     }
650
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
654             return true;
655         } else {
656             return false;
657         }
658     }
659
660     private static String getDatabaseKey(String url, long postIdentifier) {
661         assert !JniUtil.useChromiumHttpStack();
662
663         if (postIdentifier == 0) return url;
664         return postIdentifier + url;
665     }
666
667     @SuppressWarnings("deprecation")
668     private static void setupFiles(String url, CacheResult cacheRet) {
669         assert !JniUtil.useChromiumHttpStack();
670
671         if (true) {
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);
684             if (true) {
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()) {
693                     if (checkOldPath) {
694                         CacheResult oldResult = mDataBase.getCache(url);
695                         if (oldResult != null && oldResult.contentLength > 0) {
696                             if (path.equals(oldResult.localPath)) {
697                                 path = oldResult.localPath;
698                             } else {
699                                 path = oldResult.localPath;
700                                 file = new File(mBaseDir, path);
701                             }
702                             break;
703                         }
704                         checkOldPath = false;
705                     }
706                     ret = new StringBuffer(8);
707                     appendAsHex(++hashCode, ret);
708                     path = ret.toString();
709                     file = new File(mBaseDir, path);
710                 }
711             }
712             cacheRet.localPath = path;
713             cacheRet.outFile = file;
714         } else {
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);
730             }
731             cacheRet.localPath = result.toString();
732             cacheRet.outFile = new File(mBaseDir, cacheRet.localPath);
733         }
734     }
735
736     private static void appendAsHex(int i, StringBuffer ret) {
737         assert !JniUtil.useChromiumHttpStack();
738
739         String hex = Integer.toHexString(i);
740         switch (hex.length()) {
741             case 1:
742                 ret.append("0000000");
743                 break;
744             case 2:
745                 ret.append("000000");
746                 break;
747             case 3:
748                 ret.append("00000");
749                 break;
750             case 4:
751                 ret.append("0000");
752                 break;
753             case 5:
754                 ret.append("000");
755                 break;
756             case 6:
757                 ret.append("00");
758                 break;
759             case 7:
760                 ret.append("0");
761                 break;
762         }
763         ret.append(hex);
764     }
765
766     private static CacheResult parseHeaders(int statusCode, Headers headers,
767             String mimeType) {
768         assert !JniUtil.useChromiumHttpStack();
769
770         // if the contentLength is already larger than CACHE_MAX_SIZE, skip it
771         if (headers.getContentLength() > CACHE_MAX_SIZE) return null;
772
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;
781
782         // TODO: if authenticated or secure, return null
783         CacheResult ret = new CacheResult();
784         ret.httpStatusCode = statusCode;
785
786         ret.location = headers.getLocation();
787
788         ret.expires = -1;
789         ret.expiresString = headers.getExpires();
790         if (ret.expiresString != null) {
791             try {
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
798                     ret.expires = 0;
799                 } else {
800                     Log.e(LOGTAG, "illegal expires: " + ret.expiresString);
801                 }
802             }
803         }
804
805         ret.contentdisposition = headers.getContentDisposition();
806
807         ret.crossDomain = headers.getXPermittedCrossDomainPolicies();
808
809         // lastModified and etag may be set back to http header. So they can't
810         // be empty string.
811         String lastModified = headers.getLastModified();
812         if (lastModified != null && lastModified.length() > 0) {
813             ret.lastModified = lastModified;
814         }
815
816         String etag = headers.getEtag();
817         if (etag != null && etag.length() > 0) {
818             ret.etag = etag;
819         }
820
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])) {
826                     return null;
827                 }
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])) {
833                     ret.expires = 0;
834                 } else if (controls[i].startsWith(MAX_AGE)) {
835                     int separator = controls[i].indexOf('=');
836                     if (separator < 0) {
837                         separator = controls[i].indexOf(':');
838                     }
839                     if (separator > 0) {
840                         String s = controls[i].substring(separator + 1);
841                         try {
842                             long sec = Long.parseLong(s);
843                             if (sec >= 0) {
844                                 ret.expires = System.currentTimeMillis() + 1000
845                                         * sec;
846                             }
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
851                             } else {
852                                 Log.e(LOGTAG, "exception in parseHeaders for "
853                                         + "max-age:"
854                                         + controls[i].substring(separator + 1));
855                                 ret.expires = 0;
856                             }
857                         }
858                     }
859                 }
860             }
861         }
862
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())) {
867             ret.expires = 0;
868         }
869
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
879                 ret.expires = 0;
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.
884
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
889                 } else {
890                     // Setting a expires as zero will cache the result for
891                     // forward/back nav.
892                     ret.expires = 0;
893                 }
894             } else {
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%.
899
900                 // 24 * 60 * 60 * 1000
901                 long lastmod = System.currentTimeMillis() + 86400000;
902                 try {
903                     lastmod = AndroidHttpClient.parseDate(ret.lastModified);
904                 } catch (IllegalArgumentException ex) {
905                     Log.e(LOGTAG, "illegal lastModified: " + ret.lastModified);
906                 }
907                 long difference = System.currentTimeMillis() - lastmod;
908                 if (difference > 0) {
909                     ret.expires = System.currentTimeMillis() + difference / 5;
910                 } else {
911                     // last modified is in the future, expire the content
912                     // on the last modified
913                     ret.expires = lastmod;
914                 }
915             }
916         }
917
918         return ret;
919     }
920 }