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 Always returns false. 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 Always returns false. 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