Home | History | Annotate | Download | only in webkit
      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 
    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