blob: 4f680e5570132aad4c8f1303c4ac73228011f14b [file] [log] [blame]
The Android Open Source Project9066cfe2009-03-03 19:31:44 -08001/*
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;
Dianne Hackborn2269d1572010-02-24 19:54:22 -080021import android.net.http.HttpDateTime;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080022import android.os.FileUtils;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080023import android.util.Log;
24import java.io.File;
25import java.io.FileInputStream;
26import java.io.FileNotFoundException;
27import java.io.FileOutputStream;
Grace Kloba2036dba2010-02-15 02:15:37 -080028import java.io.FilenameFilter;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080029import java.io.IOException;
30import java.io.InputStream;
31import java.io.OutputStream;
Grace Kloba2036dba2010-02-15 02:15:37 -080032import java.util.List;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080033import java.util.Map;
34
Doug Zongker45a9a142010-02-03 13:52:18 -080035
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080036import 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";
Andrei Popescua1ba11b2010-02-02 15:59:26 +000058 private static final String MANIFEST_MIME = "text/cache-manifest";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080059
60 private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
61 private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
62
Grace Kloba998c05b2009-12-27 17:39:43 -080063 // 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080066 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
Ben Murdochf0c443d2009-11-19 16:43:58 +000084 /**
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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080089 public static class CacheResult {
90 // these fields are saved to the database
91 int httpStatusCode;
92 long contentLength;
93 long expires;
Grace Klobae64c5562009-06-19 15:56:08 -070094 String expiresString;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080095 String localPath;
96 String lastModified;
97 String etag;
98 String mimeType;
99 String location;
100 String encoding;
Grace Kloba0b956e12009-06-29 14:49:10 -0700101 String contentdisposition;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800102
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
Grace Klobae64c5562009-06-19 15:56:08 -0700124 public String getExpiresString() {
125 return expiresString;
126 }
127
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800128 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
Grace Kloba0b956e12009-06-29 14:49:10 -0700148 public String getContentDisposition() {
149 return contentdisposition;
150 }
151
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800152 // 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) {
Romain Guy01d0fbf2009-12-01 14:52:19 -0800177 mDataBase = WebViewDatabase.getInstance(context.getApplicationContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800178 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.
Grace Kloba2036dba2010-02-15 02:15:37 -0800204 // delete rows in the cache database
205 WebViewWorker.getHandler().sendEmptyMessage(
206 WebViewWorker.MSG_CLEAR_CACHE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800207 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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800227 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
Grace Kloba2036dba2010-02-15 02:15:37 -0800246 // only called from WebViewWorkerThread
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800247 // 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
Grace Kloba2036dba2010-02-15 02:15:37 -0800256 // only called from WebViewWorkerThread
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800257 // make sure to call enableTransaction/disableTransaction in pair
258 static boolean disableTransaction() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800259 if (--mRefCount == 0) {
260 mDataBase.endCacheTransaction();
261 return true;
262 }
263 return false;
264 }
265
Grace Kloba2036dba2010-02-15 02:15:37 -0800266 // only called from WebViewWorkerThread
267 // make sure to call startTransaction/endTransaction in pair
268 static boolean startTransaction() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800269 return mDataBase.startCacheTransaction();
270 }
271
Grace Kloba2036dba2010-02-15 02:15:37 -0800272 // only called from WebViewWorkerThread
273 // make sure to call startTransaction/endTransaction in pair
274 static boolean endTransaction() {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800275 boolean ret = mDataBase.endCacheTransaction();
276 if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) {
277 mTrimCacheCount = 0;
278 trimCacheIfNeeded();
279 }
280 return ret;
281 }
282
Grace Kloba2036dba2010-02-15 02:15:37 -0800283 // 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800303 /**
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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800311 public static CacheResult getCacheFile(String url,
312 Map<String, String> headers) {
Grace Kloba8c92c392009-11-08 19:01:55 -0800313 return getCacheFile(url, 0, headers);
314 }
315
Grace Kloba8c92c392009-11-08 19:01:55 -0800316 static CacheResult getCacheFile(String url, long postIdentifier,
317 Map<String, String> headers) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800318 if (mDisabled) {
319 return null;
320 }
321
Grace Kloba8c92c392009-11-08 19:01:55 -0800322 String databaseKey = getDatabaseKey(url, postIdentifier);
323
324 CacheResult result = mDataBase.getCache(databaseKey);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800325 if (result != null) {
326 if (result.contentLength == 0) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700327 if (!checkCacheRedirect(result.httpStatusCode)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800328 // this should not happen. If it does, remove it.
Grace Kloba8c92c392009-11-08 19:01:55 -0800329 mDataBase.removeCache(databaseKey);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800330 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
Grace Kloba8c92c392009-11-08 19:01:55 -0800341 mDataBase.removeCache(databaseKey);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800342 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
Derek Sollenberger2e5c1502009-06-03 10:44:42 -0400367 if (DebugFlags.CACHE_MANAGER) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800368 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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800386 public static CacheResult createCacheFile(String url, int statusCode,
387 Headers headers, String mimeType, boolean forceCache) {
Grace Kloba8c92c392009-11-08 19:01:55 -0800388 return createCacheFile(url, statusCode, headers, mimeType, 0,
389 forceCache);
390 }
391
Grace Kloba8c92c392009-11-08 19:01:55 -0800392 static CacheResult createCacheFile(String url, int statusCode,
393 Headers headers, String mimeType, long postIdentifier,
394 boolean forceCache) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800395 if (!forceCache && mDisabled) {
396 return null;
397 }
398
Grace Kloba8c92c392009-11-08 19:01:55 -0800399 String databaseKey = getDatabaseKey(url, postIdentifier);
400
The Android Open Source Project10592532009-03-18 17:39:46 -0700401 // according to the rfc 2616, the 303 response MUST NOT be cached.
402 if (statusCode == 303) {
Grace Kloba3afdd562009-04-02 10:55:37 -0700403 // remove the saved cache if there is any
Grace Kloba8c92c392009-11-08 19:01:55 -0800404 mDataBase.removeCache(databaseKey);
The Android Open Source Project10592532009-03-18 17:39:46 -0700405 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()) {
Grace Kloba3afdd562009-04-02 10:55:37 -0700411 // remove the saved cache if there is any
Grace Kloba8c92c392009-11-08 19:01:55 -0800412 mDataBase.removeCache(databaseKey);
The Android Open Source Project10592532009-03-18 17:39:46 -0700413 return null;
414 }
415
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800416 CacheResult ret = parseHeaders(statusCode, headers, mimeType);
Grace Kloba3afdd562009-04-02 10:55:37 -0700417 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
Grace Kloba8c92c392009-11-08 19:01:55 -0800420 mDataBase.removeCache(databaseKey);
Grace Kloba3afdd562009-04-02 10:55:37 -0700421 } else {
Grace Kloba8c92c392009-11-08 19:01:55 -0800422 setupFiles(databaseKey, ret);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800423 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 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800451 public static void saveCacheFile(String url, CacheResult cacheRet) {
Grace Kloba8c92c392009-11-08 19:01:55 -0800452 saveCacheFile(url, 0, cacheRet);
453 }
454
Grace Kloba8c92c392009-11-08 19:01:55 -0800455 static void saveCacheFile(String url, long postIdentifier,
456 CacheResult cacheRet) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800457 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
Cary Clark543221f2009-08-12 13:20:41 -0400468 boolean redirect = checkCacheRedirect(cacheRet.httpStatusCode);
469 if (redirect) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800470 // location is in database, no need to keep the file
471 cacheRet.contentLength = 0;
Cary Clark686cf752009-08-11 16:08:52 -0400472 cacheRet.localPath = "";
Cary Clark543221f2009-08-12 13:20:41 -0400473 }
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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800479 return;
480 }
481
Grace Kloba8c92c392009-11-08 19:01:55 -0800482 mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800483
Derek Sollenberger2e5c1502009-06-03 10:44:42 -0400484 if (DebugFlags.CACHE_MANAGER) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800485 Log.v(LOGTAG, "saveCacheFile for url " + url);
486 }
487 }
488
Grace Kloba998c05b2009-12-27 17:39:43 -0800489 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800498 /**
499 * remove all cache files
500 *
501 * @return true if it succeeds
502 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800503 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 }
Grace Kloba2036dba2010-02-15 02:15:37 -0800512 // 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.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800516 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++) {
Cary Clark543221f2009-08-12 13:20:41 -0400524 File f = new File(mBaseDir, files[i]);
525 if (!f.delete()) {
526 Log.e(LOGTAG, f.getPath() + " delete failed.");
527 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800528 }
529 }
530 } catch (SecurityException e) {
531 // Ignore SecurityExceptions.
532 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800533 }
534 };
535 new Thread(clearCache).start();
536 return true;
537 }
538
539 /**
540 * Return true if the cache is empty.
541 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800542 static boolean cacheEmpty() {
543 return mDataBase.hasCache();
544 }
545
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800546 static void trimCacheIfNeeded() {
547 if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) {
Grace Kloba2036dba2010-02-15 02:15:37 -0800548 List<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800549 int size = pathList.size();
550 for (int i = 0; i < size; i++) {
Cary Clark543221f2009-08-12 13:20:41 -0400551 File f = new File(mBaseDir, pathList.get(i));
552 if (!f.delete()) {
553 Log.e(LOGTAG, f.getPath() + " delete failed.");
554 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800555 }
Grace Kloba2036dba2010-02-15 02:15:37 -0800556 // 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800576 }
577 }
578
Grace Kloba2036dba2010-02-15 02:15:37 -0800579 static void clearCache() {
580 // delete database
581 mDataBase.clearCache();
582 }
583
The Android Open Source Project10592532009-03-18 17:39:46 -0700584 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
Grace Kloba8c92c392009-11-08 19:01:55 -0800593 private static String getDatabaseKey(String url, long postIdentifier) {
594 if (postIdentifier == 0) return url;
595 return postIdentifier + url;
596 }
597
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800598 @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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800623 CacheResult oldResult = mDataBase.getCache(url);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800624 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) {
Grace Kloba998c05b2009-12-27 17:39:43 -0800695 // if the contentLength is already larger than CACHE_MAX_SIZE, skip it
696 if (headers.getContentLength() > CACHE_MAX_SIZE) return null;
697
Andrei Popescua1ba11b2010-02-02 15:59:26 +0000698 // 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800707 // 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;
Grace Klobae64c5562009-06-19 15:56:08 -0700715 ret.expiresString = headers.getExpires();
716 if (ret.expiresString != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800717 try {
Grace Klobae64c5562009-06-19 15:56:08 -0700718 ret.expires = HttpDateTime.parse(ret.expiresString);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800719 } catch (IllegalArgumentException ex) {
720 // Take care of the special "-1" and "0" cases
Grace Klobae64c5562009-06-19 15:56:08 -0700721 if ("-1".equals(ret.expiresString)
722 || "0".equals(ret.expiresString)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800723 // make it expired, but can be used for history navigation
724 ret.expires = 0;
725 } else {
Grace Klobae64c5562009-06-19 15:56:08 -0700726 Log.e(LOGTAG, "illegal expires: " + ret.expiresString);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800727 }
728 }
729 }
730
Grace Kloba0b956e12009-06-29 14:49:10 -0700731 String contentDisposition = headers.getContentDisposition();
732 if (contentDisposition != null) {
733 ret.contentdisposition = contentDisposition;
734 }
735
Grace Kloba7865fa92010-03-19 19:48:28 -0700736 // lastModified and etag may be set back to http header. So they can't
737 // be empty string.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800738 String lastModified = headers.getLastModified();
Grace Kloba7865fa92010-03-19 19:48:28 -0700739 if (lastModified != null && lastModified.length() > 0) {
740 ret.lastModified = lastModified;
741 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800742
743 String etag = headers.getEtag();
Grace Kloba7865fa92010-03-19 19:48:28 -0700744 if (etag != null && etag.length() > 0) ret.etag = etag;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800745
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
Grace Kloba52cf58a2009-03-25 20:05:44 -0700757 if (NO_CACHE.equals(controls[i])) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800758 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}