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