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