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