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