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