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