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