• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2006 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.webkit;
18 
19 import android.content.Context;
20 import android.net.http.AndroidHttpClient;
21 import android.net.http.Headers;
22 import android.os.FileUtils;
23 import android.util.Log;
24 import java.io.File;
25 import java.io.FileInputStream;
26 import java.io.FileNotFoundException;
27 import java.io.FileOutputStream;
28 import java.io.FilenameFilter;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.util.List;
33 import java.util.Map;
34 
35 
36 import com.android.org.bouncycastle.crypto.Digest;
37 import 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
53 public 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 
getHttpStatusCode()117         public int getHttpStatusCode() {
118             return httpStatusCode;
119         }
120 
getContentLength()121         public long getContentLength() {
122             return contentLength;
123         }
124 
getLocalPath()125         public String getLocalPath() {
126             return localPath;
127         }
128 
getExpires()129         public long getExpires() {
130             return expires;
131         }
132 
getExpiresString()133         public String getExpiresString() {
134             return expiresString;
135         }
136 
getLastModified()137         public String getLastModified() {
138             return lastModified;
139         }
140 
getETag()141         public String getETag() {
142             return etag;
143         }
144 
getMimeType()145         public String getMimeType() {
146             return mimeType;
147         }
148 
getLocation()149         public String getLocation() {
150             return location;
151         }
152 
getEncoding()153         public String getEncoding() {
154             return encoding;
155         }
156 
getContentDisposition()157         public String getContentDisposition() {
158             return contentdisposition;
159         }
160 
161         // For out-of-package access to the underlying streams.
getInputStream()162         public InputStream getInputStream() {
163             return inStream;
164         }
165 
getOutputStream()166         public OutputStream getOutputStream() {
167             return outStream;
168         }
169 
170         // These fields can be set manually.
setInputStream(InputStream stream)171         public void setInputStream(InputStream stream) {
172             this.inStream = stream;
173         }
174 
setEncoding(String encoding)175         public void setEncoding(String encoding) {
176             this.encoding = encoding;
177         }
178 
179         /**
180          * @hide
181          */
setContentLength(long contentLength)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      */
init(Context context)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      */
createCacheDirectory()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
getCacheFileBaseDir()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      */
setCacheDisabled(boolean disabled)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
cacheDisabled()284     public static boolean cacheDisabled() {
285         return mDisabled;
286     }
287 
288     // only called from WebViewWorkerThread
289     // make sure to call enableTransaction/disableTransaction in pair
enableTransaction()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
disableTransaction()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
startTransaction()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
endTransaction()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
startCacheTransaction()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
endCacheTransaction()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
getCacheFile(String url, Map<String, String> headers)366     public static CacheResult getCacheFile(String url,
367             Map<String, String> headers) {
368         return getCacheFile(url, 0, headers);
369     }
370 
getCacheFile(String url, long postIdentifier, Map<String, String> headers)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
createCacheFile(String url, int statusCode, Headers headers, String mimeType, boolean forceCache)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 
createCacheFile(String url, int statusCode, Headers headers, String mimeType, long postIdentifier, boolean forceCache)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
saveCacheFile(String url, CacheResult cacheRet)538     public static void saveCacheFile(String url, CacheResult cacheRet) {
539         saveCacheFile(url, 0, cacheRet);
540     }
541 
saveCacheFile(String url, long postIdentifier, CacheResult cacheRet)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 
cleanupCacheFile(CacheResult cacheRet)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      */
removeAllCacheFiles()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 
trimCacheIfNeeded()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 
clearCache()680     static void clearCache() {
681         assert !JniUtil.useChromiumHttpStack();
682 
683         // delete database
684         mDataBase.clearCache();
685     }
686 
isCachableRedirect(int statusCode)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 
getDatabaseKey(String url, long postIdentifier)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")
setupFiles(String url, CacheResult cacheRet)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 
appendAsHex(int i, StringBuffer ret)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 
parseHeaders(int statusCode, Headers headers, String mimeType)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 
nativeGetCacheResult(String url)963     private static native CacheResult nativeGetCacheResult(String url);
964 }
965