blob: e21a02e8d8b74f0033b23c76533b42311fb61899 [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;
Jesse Wilson7cfa90f2010-04-08 14:20:57 -070020import android.net.http.AndroidHttpClient;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080021import android.net.http.Headers;
22import 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
Brian Carlstrom4140fae2011-01-24 16:17:43 -080036import com.android.org.bouncycastle.crypto.Digest;
37import com.android.org.bouncycastle.crypto.digests.SHA1Digest;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080038
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.
Iain Merrickc96235d2010-12-13 17:27:14 +000047 *
Iain Merrick0e22f2b2011-01-18 14:47:50 +000048 * CacheManager may only be used if your activity contains a WebView.
49 *
Iain Merrickc96235d2010-12-13 17:27:14 +000050 * @deprecated Access to the HTTP cache will be removed in a future release.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080051 */
Iain Merrickc96235d2010-12-13 17:27:14 +000052@Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080053public 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";
Andrei Popescua1ba11b2010-02-02 15:59:26 +000063 private static final String MANIFEST_MIME = "text/cache-manifest";
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080064
65 private static long CACHE_THRESHOLD = 6 * 1024 * 1024;
66 private static long CACHE_TRIM_AMOUNT = 2 * 1024 * 1024;
67
Grace Kloba998c05b2009-12-27 17:39:43 -080068 // 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080071 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
Ben Murdochf0c443d2009-11-19 16:43:58 +000089 /**
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.
Iain Merrickc96235d2010-12-13 17:27:14 +000093 *
94 * @deprecated Access to the HTTP cache will be removed in a future release.
Ben Murdochf0c443d2009-11-19 16:43:58 +000095 */
Iain Merrickc96235d2010-12-13 17:27:14 +000096 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -080097 public static class CacheResult {
98 // these fields are saved to the database
99 int httpStatusCode;
100 long contentLength;
101 long expires;
Grace Klobae64c5562009-06-19 15:56:08 -0700102 String expiresString;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800103 String localPath;
104 String lastModified;
105 String etag;
106 String mimeType;
107 String location;
108 String encoding;
Grace Kloba0b956e12009-06-29 14:49:10 -0700109 String contentdisposition;
Leon Clarke60708a72010-03-23 18:35:04 +0000110 String crossDomain;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800111
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
Grace Klobae64c5562009-06-19 15:56:08 -0700133 public String getExpiresString() {
134 return expiresString;
135 }
136
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800137 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
Grace Kloba0b956e12009-06-29 14:49:10 -0700157 public String getContentDisposition() {
158 return contentdisposition;
159 }
160
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800161 // 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 }
Iain Merrickb67529b2010-12-17 11:10:17 +0000178
179 /**
180 * @hide
181 */
182 public void setContentLength(long contentLength) {
183 this.contentLength = contentLength;
184 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800185 }
186
187 /**
Steve Block67ba2042011-01-04 14:24:05 +0000188 * Initialize the CacheManager.
189 *
190 * Note that this is called automatically when a {@link android.webkit.WebView} is created.
191 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800192 * @param context The application context.
193 */
194 static void init(Context context) {
Steve Block808751f2011-01-04 14:26:27 +0000195 if (JniUtil.useChromiumHttpStack()) {
Steve Blocke8492472011-01-07 12:30:35 +0000196 // 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();
Steve Blocke8492472011-01-07 12:30:35 +0000201 }
Steve Block808751f2011-01-04 14:26:27 +0000202 return;
203 }
204
Romain Guy01d0fbf2009-12-01 14:52:19 -0800205 mDataBase = WebViewDatabase.getInstance(context.getApplicationContext());
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800206 mBaseDir = new File(context.getCacheDir(), "webviewCache");
207 if (createCacheDirectory() && mClearCacheOnInit) {
208 removeAllCacheFiles();
209 mClearCacheOnInit = false;
210 }
211 }
Steve Block67ba2042011-01-04 14:24:05 +0000212
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800213 /**
214 * Create the cache directory if it does not already exist.
Steve Block67ba2042011-01-04 14:24:05 +0000215 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800216 * @return true if the cache directory didn't exist and was created.
217 */
218 static private boolean createCacheDirectory() {
Steve Block808751f2011-01-04 14:26:27 +0000219 assert !JniUtil.useChromiumHttpStack();
220
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800221 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(),
John Reck11c1a0b2010-12-07 14:53:17 -0800228 FileUtils.S_IRWXU | FileUtils.S_IRWXG,
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800229 -1, -1);
Steve Block67ba2042011-01-04 14:24:05 +0000230 // If we did create the directory, we need to flush
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800231 // the cache database. The directory could be recreated
232 // because the system flushed all the data/cache directories
233 // to free up disk space.
Grace Kloba2036dba2010-02-15 02:15:37 -0800234 // delete rows in the cache database
235 WebViewWorker.getHandler().sendEmptyMessage(
236 WebViewWorker.MSG_CLEAR_CACHE);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800237 return true;
238 }
239 return false;
240 }
241
242 /**
Steve Block67ba2042011-01-04 14:24:05 +0000243 * 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.
Iain Merrickc96235d2010-12-13 17:27:14 +0000246 *
Kristian Monsencedb3a72011-01-20 12:42:16 +0000247 * Cache files are not guaranteed to be in this directory before
248 * CacheManager#getCacheFile(String, Map<String, String>) is called.
249 *
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800250 * @return File The base directory of the cache.
Iain Merrickc96235d2010-12-13 17:27:14 +0000251 *
252 * @deprecated Access to the HTTP cache will be removed in a future release.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800253 */
Iain Merrickc96235d2010-12-13 17:27:14 +0000254 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800255 public static File getCacheFileBaseDir() {
256 return mBaseDir;
257 }
258
259 /**
Steve Block67ba2042011-01-04 14:24:05 +0000260 * Sets whether the cache is disabled.
261 *
262 * @param disabled Whether the cache should be disabled
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800263 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800264 static void setCacheDisabled(boolean disabled) {
Steve Block808751f2011-01-04 14:26:27 +0000265 assert !JniUtil.useChromiumHttpStack();
266
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800267 if (disabled == mDisabled) {
268 return;
269 }
270 mDisabled = disabled;
271 if (mDisabled) {
272 removeAllCacheFiles();
273 }
274 }
275
276 /**
Steve Block67ba2042011-01-04 14:24:05 +0000277 * Whether the cache is disabled.
Iain Merrickc96235d2010-12-13 17:27:14 +0000278 *
Steve Block67ba2042011-01-04 14:24:05 +0000279 * @return return Whether the cache is disabled
Iain Merrickc96235d2010-12-13 17:27:14 +0000280 *
281 * @deprecated Access to the HTTP cache will be removed in a future release.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800282 */
Iain Merrickc96235d2010-12-13 17:27:14 +0000283 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800284 public static boolean cacheDisabled() {
285 return mDisabled;
286 }
287
Grace Kloba2036dba2010-02-15 02:15:37 -0800288 // only called from WebViewWorkerThread
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800289 // make sure to call enableTransaction/disableTransaction in pair
290 static boolean enableTransaction() {
Steve Block808751f2011-01-04 14:26:27 +0000291 assert !JniUtil.useChromiumHttpStack();
292
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800293 if (++mRefCount == 1) {
294 mDataBase.startCacheTransaction();
295 return true;
296 }
297 return false;
298 }
299
Grace Kloba2036dba2010-02-15 02:15:37 -0800300 // only called from WebViewWorkerThread
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800301 // make sure to call enableTransaction/disableTransaction in pair
302 static boolean disableTransaction() {
Steve Block808751f2011-01-04 14:26:27 +0000303 assert !JniUtil.useChromiumHttpStack();
304
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800305 if (--mRefCount == 0) {
306 mDataBase.endCacheTransaction();
307 return true;
308 }
309 return false;
310 }
311
Grace Kloba2036dba2010-02-15 02:15:37 -0800312 // only called from WebViewWorkerThread
313 // make sure to call startTransaction/endTransaction in pair
314 static boolean startTransaction() {
Steve Block808751f2011-01-04 14:26:27 +0000315 assert !JniUtil.useChromiumHttpStack();
316
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800317 return mDataBase.startCacheTransaction();
318 }
319
Grace Kloba2036dba2010-02-15 02:15:37 -0800320 // only called from WebViewWorkerThread
321 // make sure to call startTransaction/endTransaction in pair
322 static boolean endTransaction() {
Steve Block808751f2011-01-04 14:26:27 +0000323 assert !JniUtil.useChromiumHttpStack();
324
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800325 boolean ret = mDataBase.endCacheTransaction();
326 if (++mTrimCacheCount >= TRIM_CACHE_INTERVAL) {
327 mTrimCacheCount = 0;
328 trimCacheIfNeeded();
329 }
330 return ret;
331 }
332
Grace Kloba2036dba2010-02-15 02:15:37 -0800333 // only called from WebCore Thread
334 // make sure to call startCacheTransaction/endCacheTransaction in pair
335 /**
Joe Onorato201480c2010-09-10 15:01:35 -0400336 * @deprecated Always returns false.
Grace Kloba2036dba2010-02-15 02:15:37 -0800337 */
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 /**
Joe Onorato201480c2010-09-10 15:01:35 -0400346 * @deprecated Always returns false.
Grace Kloba2036dba2010-02-15 02:15:37 -0800347 */
348 @Deprecated
349 public static boolean endCacheTransaction() {
350 return false;
351 }
352
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800353 /**
Steve Block67ba2042011-01-04 14:24:05 +0000354 * Given a URL, returns the corresponding CacheResult if it exists, or null otherwise.
355 *
Steve Blocke8492472011-01-07 12:30:35 +0000356 * 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.
Steve Block67ba2042011-01-04 14:24:05 +0000358 * 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
Iain Merrickc96235d2010-12-13 17:27:14 +0000362 *
363 * @deprecated Access to the HTTP cache will be removed in a future release.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800364 */
Iain Merrickc96235d2010-12-13 17:27:14 +0000365 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800366 public static CacheResult getCacheFile(String url,
367 Map<String, String> headers) {
Grace Kloba8c92c392009-11-08 19:01:55 -0800368 return getCacheFile(url, 0, headers);
369 }
370
Grace Kloba8c92c392009-11-08 19:01:55 -0800371 static CacheResult getCacheFile(String url, long postIdentifier,
372 Map<String, String> headers) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800373 if (mDisabled) {
374 return null;
375 }
376
Steve Block808751f2011-01-04 14:26:27 +0000377 if (JniUtil.useChromiumHttpStack()) {
Steve Blocke8492472011-01-07 12:30:35 +0000378 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;
Steve Block808751f2011-01-04 14:26:27 +0000396 }
Grace Kloba8c92c392009-11-08 19:01:55 -0800397
Steve Block808751f2011-01-04 14:26:27 +0000398 String databaseKey = getDatabaseKey(url, postIdentifier);
Grace Kloba8c92c392009-11-08 19:01:55 -0800399 CacheResult result = mDataBase.getCache(databaseKey);
Steve Block67ba2042011-01-04 14:24:05 +0000400 if (result == null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800401 return null;
402 }
Steve Block67ba2042011-01-04 14:24:05 +0000403 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800422
Steve Block67ba2042011-01-04 14:24:05 +0000423 // 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.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800426 if (headers != null && result.expires >= 0
427 && result.expires <= System.currentTimeMillis()) {
428 if (result.lastModified == null && result.etag == null) {
429 return null;
430 }
Steve Block67ba2042011-01-04 14:24:05 +0000431 // Return HEADER_KEY_IFNONEMATCH or HEADER_KEY_IFMODIFIEDSINCE
432 // for requesting validation.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800433 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
Derek Sollenberger2e5c1502009-06-03 10:44:42 -0400441 if (DebugFlags.CACHE_MANAGER) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800442 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.
Iain Merrickc96235d2010-12-13 17:27:14 +0000459 *
460 * @deprecated Access to the HTTP cache will be removed in a future release.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800461 */
Iain Merrickc96235d2010-12-13 17:27:14 +0000462 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800463 public static CacheResult createCacheFile(String url, int statusCode,
464 Headers headers, String mimeType, boolean forceCache) {
Steve Block808751f2011-01-04 14:26:27 +0000465 if (JniUtil.useChromiumHttpStack()) {
Steve Blocke8492472011-01-07 12:30:35 +0000466 // This method is public but hidden. We break functionality.
Steve Block808751f2011-01-04 14:26:27 +0000467 return null;
468 }
469
Grace Kloba8c92c392009-11-08 19:01:55 -0800470 return createCacheFile(url, statusCode, headers, mimeType, 0,
471 forceCache);
472 }
473
Grace Kloba8c92c392009-11-08 19:01:55 -0800474 static CacheResult createCacheFile(String url, int statusCode,
475 Headers headers, String mimeType, long postIdentifier,
476 boolean forceCache) {
Steve Block808751f2011-01-04 14:26:27 +0000477 assert !JniUtil.useChromiumHttpStack();
478
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800479 if (!forceCache && mDisabled) {
480 return null;
481 }
482
Grace Kloba8c92c392009-11-08 19:01:55 -0800483 String databaseKey = getDatabaseKey(url, postIdentifier);
484
The Android Open Source Project10592532009-03-18 17:39:46 -0700485 // according to the rfc 2616, the 303 response MUST NOT be cached.
486 if (statusCode == 303) {
Grace Kloba3afdd562009-04-02 10:55:37 -0700487 // remove the saved cache if there is any
Grace Kloba8c92c392009-11-08 19:01:55 -0800488 mDataBase.removeCache(databaseKey);
The Android Open Source Project10592532009-03-18 17:39:46 -0700489 return null;
490 }
491
492 // like the other browsers, do not cache redirects containing a cookie
493 // header.
Steve Block67ba2042011-01-04 14:24:05 +0000494 if (isCachableRedirect(statusCode) && !headers.getSetCookie().isEmpty()) {
Grace Kloba3afdd562009-04-02 10:55:37 -0700495 // remove the saved cache if there is any
Grace Kloba8c92c392009-11-08 19:01:55 -0800496 mDataBase.removeCache(databaseKey);
The Android Open Source Project10592532009-03-18 17:39:46 -0700497 return null;
498 }
499
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800500 CacheResult ret = parseHeaders(statusCode, headers, mimeType);
Grace Kloba3afdd562009-04-02 10:55:37 -0700501 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
Grace Kloba8c92c392009-11-08 19:01:55 -0800504 mDataBase.removeCache(databaseKey);
Grace Kloba3afdd562009-04-02 10:55:37 -0700505 } else {
Grace Kloba8c92c392009-11-08 19:01:55 -0800506 setupFiles(databaseKey, ret);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800507 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
Iain Merrickc96235d2010-12-13 17:27:14 +0000534 *
535 * @deprecated Access to the HTTP cache will be removed in a future release.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800536 */
Iain Merrickc96235d2010-12-13 17:27:14 +0000537 @Deprecated
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800538 public static void saveCacheFile(String url, CacheResult cacheRet) {
Grace Kloba8c92c392009-11-08 19:01:55 -0800539 saveCacheFile(url, 0, cacheRet);
540 }
541
Grace Kloba8c92c392009-11-08 19:01:55 -0800542 static void saveCacheFile(String url, long postIdentifier,
543 CacheResult cacheRet) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800544 try {
545 cacheRet.outStream.close();
546 } catch (IOException e) {
547 return;
548 }
549
Steve Block808751f2011-01-04 14:26:27 +0000550 if (JniUtil.useChromiumHttpStack()) {
Steve Blocke8492472011-01-07 12:30:35 +0000551 // 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;
Steve Block808751f2011-01-04 14:26:27 +0000561 }
562
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800563 if (!cacheRet.outFile.exists()) {
564 // the file in the cache directory can be removed by the system
565 return;
566 }
567
Steve Block67ba2042011-01-04 14:24:05 +0000568 boolean redirect = isCachableRedirect(cacheRet.httpStatusCode);
Cary Clark543221f2009-08-12 13:20:41 -0400569 if (redirect) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800570 // location is in database, no need to keep the file
571 cacheRet.contentLength = 0;
Cary Clark686cf752009-08-11 16:08:52 -0400572 cacheRet.localPath = "";
Cary Clark543221f2009-08-12 13:20:41 -0400573 }
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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800579 return;
580 }
581
Grace Kloba8c92c392009-11-08 19:01:55 -0800582 mDataBase.addCache(getDatabaseKey(url, postIdentifier), cacheRet);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800583
Derek Sollenberger2e5c1502009-06-03 10:44:42 -0400584 if (DebugFlags.CACHE_MANAGER) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800585 Log.v(LOGTAG, "saveCacheFile for url " + url);
586 }
587 }
588
Grace Kloba998c05b2009-12-27 17:39:43 -0800589 static boolean cleanupCacheFile(CacheResult cacheRet) {
Steve Block808751f2011-01-04 14:26:27 +0000590 assert !JniUtil.useChromiumHttpStack();
591
Grace Kloba998c05b2009-12-27 17:39:43 -0800592 try {
593 cacheRet.outStream.close();
594 } catch (IOException e) {
595 return false;
596 }
597 return cacheRet.outFile.delete();
598 }
599
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800600 /**
Steve Block67ba2042011-01-04 14:24:05 +0000601 * Remove all cache files.
602 *
603 * @return Whether the removal succeeded.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800604 */
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800605 static boolean removeAllCacheFiles() {
606 // Note, this is called before init() when the database is
607 // created or upgraded.
608 if (mBaseDir == null) {
Kristian Monsen8beb86c2011-02-16 10:59:56 +0000609 // This method should not be called before init() when using the
610 // chrome http stack
611 assert !JniUtil.useChromiumHttpStack();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800612 // 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 }
Grace Kloba2036dba2010-02-15 02:15:37 -0800617 // delete rows in the cache database
Kristian Monsencedb3a72011-01-20 12:42:16 +0000618 if (!JniUtil.useChromiumHttpStack())
619 WebViewWorker.getHandler().sendEmptyMessage(WebViewWorker.MSG_CLEAR_CACHE);
620
Grace Kloba2036dba2010-02-15 02:15:37 -0800621 // delete cache files in a separate thread to not block UI.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800622 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++) {
Cary Clark543221f2009-08-12 13:20:41 -0400630 File f = new File(mBaseDir, files[i]);
631 if (!f.delete()) {
632 Log.e(LOGTAG, f.getPath() + " delete failed.");
633 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800634 }
635 }
636 } catch (SecurityException e) {
637 // Ignore SecurityExceptions.
638 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800639 }
640 };
641 new Thread(clearCache).start();
642 return true;
643 }
644
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800645 static void trimCacheIfNeeded() {
Steve Block808751f2011-01-04 14:26:27 +0000646 assert !JniUtil.useChromiumHttpStack();
647
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800648 if (mDataBase.getCacheTotalSize() > CACHE_THRESHOLD) {
Grace Kloba2036dba2010-02-15 02:15:37 -0800649 List<String> pathList = mDataBase.trimCache(CACHE_TRIM_AMOUNT);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800650 int size = pathList.size();
651 for (int i = 0; i < size; i++) {
Cary Clark543221f2009-08-12 13:20:41 -0400652 File f = new File(mBaseDir, pathList.get(i));
653 if (!f.delete()) {
654 Log.e(LOGTAG, f.getPath() + " delete failed.");
655 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800656 }
Grace Kloba2036dba2010-02-15 02:15:37 -0800657 // 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 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800677 }
678 }
679
Grace Kloba2036dba2010-02-15 02:15:37 -0800680 static void clearCache() {
Steve Block808751f2011-01-04 14:26:27 +0000681 assert !JniUtil.useChromiumHttpStack();
682
Grace Kloba2036dba2010-02-15 02:15:37 -0800683 // delete database
684 mDataBase.clearCache();
685 }
686
Steve Block67ba2042011-01-04 14:24:05 +0000687 private static boolean isCachableRedirect(int statusCode) {
The Android Open Source Project10592532009-03-18 17:39:46 -0700688 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
Grace Kloba8c92c392009-11-08 19:01:55 -0800696 private static String getDatabaseKey(String url, long postIdentifier) {
Steve Block808751f2011-01-04 14:26:27 +0000697 assert !JniUtil.useChromiumHttpStack();
698
Grace Kloba8c92c392009-11-08 19:01:55 -0800699 if (postIdentifier == 0) return url;
700 return postIdentifier + url;
701 }
702
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800703 @SuppressWarnings("deprecation")
704 private static void setupFiles(String url, CacheResult cacheRet) {
Steve Block808751f2011-01-04 14:26:27 +0000705 assert !JniUtil.useChromiumHttpStack();
706
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800707 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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800730 CacheResult oldResult = mDataBase.getCache(url);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800731 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) {
Steve Block808751f2011-01-04 14:26:27 +0000773 assert !JniUtil.useChromiumHttpStack();
774
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800775 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) {
Steve Block808751f2011-01-04 14:26:27 +0000804 assert !JniUtil.useChromiumHttpStack();
805
Grace Kloba998c05b2009-12-27 17:39:43 -0800806 // if the contentLength is already larger than CACHE_MAX_SIZE, skip it
807 if (headers.getContentLength() > CACHE_MAX_SIZE) return null;
808
Andrei Popescua1ba11b2010-02-02 15:59:26 +0000809 // 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
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800818 // TODO: if authenticated or secure, return null
819 CacheResult ret = new CacheResult();
820 ret.httpStatusCode = statusCode;
821
Steve Block7a112152011-01-05 19:36:19 +0000822 ret.location = headers.getLocation();
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800823
824 ret.expires = -1;
Grace Klobae64c5562009-06-19 15:56:08 -0700825 ret.expiresString = headers.getExpires();
826 if (ret.expiresString != null) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800827 try {
Jesse Wilson7cfa90f2010-04-08 14:20:57 -0700828 ret.expires = AndroidHttpClient.parseDate(ret.expiresString);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800829 } catch (IllegalArgumentException ex) {
830 // Take care of the special "-1" and "0" cases
Grace Klobae64c5562009-06-19 15:56:08 -0700831 if ("-1".equals(ret.expiresString)
832 || "0".equals(ret.expiresString)) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800833 // make it expired, but can be used for history navigation
834 ret.expires = 0;
835 } else {
Grace Klobae64c5562009-06-19 15:56:08 -0700836 Log.e(LOGTAG, "illegal expires: " + ret.expiresString);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800837 }
838 }
839 }
840
Steve Block7a112152011-01-05 19:36:19 +0000841 ret.contentdisposition = headers.getContentDisposition();
Grace Kloba0b956e12009-06-29 14:49:10 -0700842
Steve Block7a112152011-01-05 19:36:19 +0000843 ret.crossDomain = headers.getXPermittedCrossDomainPolicies();
Leon Clarke60708a72010-03-23 18:35:04 +0000844
Grace Kloba7865fa92010-03-19 19:48:28 -0700845 // lastModified and etag may be set back to http header. So they can't
846 // be empty string.
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800847 String lastModified = headers.getLastModified();
Grace Kloba7865fa92010-03-19 19:48:28 -0700848 if (lastModified != null && lastModified.length() > 0) {
849 ret.lastModified = lastModified;
850 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800851
852 String etag = headers.getEtag();
Steve Block7a112152011-01-05 19:36:19 +0000853 if (etag != null && etag.length() > 0) {
854 ret.etag = etag;
855 }
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800856
857 String cacheControl = headers.getCacheControl();
858 if (cacheControl != null) {
859 String[] controls = cacheControl.toLowerCase().split("[ ,;]");
Henrik Baardeb0ced72010-08-16 13:18:10 +0200860 boolean noCache = false;
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800861 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
Grace Kloba52cf58a2009-03-25 20:05:44 -0700869 if (NO_CACHE.equals(controls[i])) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800870 ret.expires = 0;
Henrik Baardeb0ced72010-08-16 13:18:10 +0200871 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) {
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800877 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 {
Jesse Wilson7cfa90f2010-04-08 14:20:57 -0700945 lastmod = AndroidHttpClient.parseDate(ret.lastModified);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800946 } 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 }
Steve Blocke8492472011-01-07 12:30:35 +0000962
963 private static native CacheResult nativeGetCacheResult(String url);
The Android Open Source Project9066cfe2009-03-03 19:31:44 -0800964}