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