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