CacheManager.java revision 811665e905f794e3ea20659ae78d322002bd329d
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.AndroidHttpClient;
21import android.net.http.Headers;
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        String crossDomain;
103
104        // these fields are NOT saved to the database
105        InputStream inStream;
106        OutputStream outStream;
107        File outFile;
108
109        public int getHttpStatusCode() {
110            return httpStatusCode;
111        }
112
113        public long getContentLength() {
114            return contentLength;
115        }
116
117        public String getLocalPath() {
118            return localPath;
119        }
120
121        public long getExpires() {
122            return expires;
123        }
124
125        public String getExpiresString() {
126            return expiresString;
127        }
128
129        public String getLastModified() {
130            return lastModified;
131        }
132
133        public String getETag() {
134            return etag;
135        }
136
137        public String getMimeType() {
138            return mimeType;
139        }
140
141        public String getLocation() {
142            return location;
143        }
144
145        public String getEncoding() {
146            return encoding;
147        }
148
149        public String getContentDisposition() {
150            return contentdisposition;
151        }
152
153        // For out-of-package access to the underlying streams.
154        public InputStream getInputStream() {
155            return inStream;
156        }
157
158        public OutputStream getOutputStream() {
159            return outStream;
160        }
161
162        // These fields can be set manually.
163        public void setInputStream(InputStream stream) {
164            this.inStream = stream;
165        }
166
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     */
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     */
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,
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     */
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     */
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     */
243    public static boolean cacheDisabled() {
244        return mDisabled;
245    }
246
247    // only called from WebViewWorkerThread
248    // make sure to call enableTransaction/disableTransaction in pair
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
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
269    static boolean startTransaction() {
270        return mDataBase.startCacheTransaction();
271    }
272
273    // only called from WebViewWorkerThread
274    // make sure to call startTransaction/endTransaction in pair
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
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
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     */
312    public static CacheResult getCacheFile(String url,
313            Map<String, String> headers) {
314        return getCacheFile(url, 0, headers);
315    }
316
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     */
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
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     */
452    public static void saveCacheFile(String url, CacheResult cacheRet) {
453        saveCacheFile(url, 0, cacheRet);
454    }
455
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
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     */
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     */
543    static boolean cacheEmpty() {
544        return mDataBase.hasCache();
545    }
546
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
580    static void clearCache() {
581        // delete database
582        mDataBase.clearCache();
583    }
584
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
594    private static String getDatabaseKey(String url, long postIdentifier) {
595        if (postIdentifier == 0) return url;
596        return postIdentifier + url;
597    }
598
599    @SuppressWarnings("deprecation")
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
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
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