blob: 2a4761bd4db109ab4e283ee76073c58c86955a61 [file] [log] [blame]
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -08001/*
2 * Copyright (C) 2011 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 com.android.volley.toolbox;
18
19import android.os.SystemClock;
20
21import com.android.volley.Cache;
22import com.android.volley.VolleyLog;
23
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -070024import java.io.EOFException;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -080025import java.io.File;
26import java.io.FileInputStream;
27import java.io.FileOutputStream;
28import java.io.FilterInputStream;
29import java.io.IOException;
30import java.io.InputStream;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -080031import java.io.OutputStream;
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -080032import java.util.Collections;
33import java.util.HashMap;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -080034import java.util.Iterator;
35import java.util.LinkedHashMap;
36import java.util.Map;
37
38/**
39 * Cache implementation that caches files directly onto the hard disk in the specified
40 * directory. The default disk usage size is 5MB, but is configurable.
41 */
42public class DiskBasedCache implements Cache {
43
44 /** Map of the Key, CacheHeader pairs */
45 private final Map<String, CacheHeader> mEntries =
46 new LinkedHashMap<String, CacheHeader>(16, .75f, true);
47
48 /** Total amount of space currently used by the cache in bytes. */
49 private long mTotalSize = 0;
50
51 /** The root directory to use for the cache. */
52 private final File mRootDirectory;
53
54 /** The maximum size of the cache in bytes. */
55 private final int mMaxCacheSizeInBytes;
56
57 /** Default maximum disk usage in bytes. */
58 private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
59
60 /** High water mark percentage for the cache */
61 private static final float HYSTERESIS_FACTOR = 0.9f;
62
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -070063 /** Magic number for current version of cache file format. */
64 private static final int CACHE_MAGIC = 0x20120504;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -080065
66 /**
67 * Constructs an instance of the DiskBasedCache at the specified directory.
68 * @param rootDirectory The root directory of the cache.
69 * @param maxCacheSizeInBytes The maximum size of the cache in bytes.
70 */
71 public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
72 mRootDirectory = rootDirectory;
73 mMaxCacheSizeInBytes = maxCacheSizeInBytes;
74 }
75
76 /**
77 * Constructs an instance of the DiskBasedCache at the specified directory using
78 * the default maximum cache size of 5MB.
79 * @param rootDirectory The root directory of the cache.
80 */
81 public DiskBasedCache(File rootDirectory) {
82 this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
83 }
84
85 /**
86 * Clears the cache. Deletes all cached files from disk.
87 */
88 @Override
89 public synchronized void clear() {
90 File[] files = mRootDirectory.listFiles();
91 if (files != null) {
92 for (File file : files) {
93 file.delete();
94 }
95 }
96 mEntries.clear();
97 mTotalSize = 0;
98 VolleyLog.d("Cache cleared.");
99 }
100
101 /**
102 * Returns the cache entry with the specified key if it exists, null otherwise.
103 */
104 @Override
105 public synchronized Entry get(String key) {
106 CacheHeader entry = mEntries.get(key);
107 // if the entry does not exist, return.
108 if (entry == null) {
109 return null;
110 }
111
112 File file = getFileForKey(key);
113 CountingInputStream cis = null;
114 try {
115 cis = new CountingInputStream(new FileInputStream(file));
116 CacheHeader.readHeader(cis); // eat header
117 byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
118 return entry.toCacheEntry(data);
119 } catch (IOException e) {
120 VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
121 remove(key);
122 return null;
123 } finally {
124 if (cis != null) {
125 try {
126 cis.close();
127 } catch (IOException ioe) {
128 return null;
129 }
130 }
131 }
132 }
133
134 /**
135 * Initializes the DiskBasedCache by scanning for all files currently in the
Ficus Kirkpatrick445ed342013-03-27 17:47:19 -0700136 * specified root directory. Creates the root directory if necessary.
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800137 */
138 @Override
139 public synchronized void initialize() {
Ficus Kirkpatrick445ed342013-03-27 17:47:19 -0700140 if (!mRootDirectory.exists()) {
141 if (!mRootDirectory.mkdirs()) {
142 VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
143 }
144 return;
145 }
146
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800147 File[] files = mRootDirectory.listFiles();
148 if (files == null) {
149 return;
150 }
151 for (File file : files) {
152 FileInputStream fis = null;
153 try {
154 fis = new FileInputStream(file);
155 CacheHeader entry = CacheHeader.readHeader(fis);
156 entry.size = file.length();
157 putEntry(entry.key, entry);
158 } catch (IOException e) {
159 if (file != null) {
160 file.delete();
161 }
162 } finally {
163 try {
164 if (fis != null) {
165 fis.close();
166 }
167 } catch (IOException ignored) { }
168 }
169 }
170 }
171
172 /**
173 * Invalidates an entry in the cache.
174 * @param key Cache key
175 * @param fullExpire True to fully expire the entry, false to soft expire
176 */
177 @Override
178 public synchronized void invalidate(String key, boolean fullExpire) {
179 Entry entry = get(key);
180 if (entry != null) {
181 entry.softTtl = 0;
182 if (fullExpire) {
183 entry.ttl = 0;
184 }
185 put(key, entry);
186 }
187
188 }
189
190 /**
191 * Puts the entry with the specified key into the cache.
192 */
193 @Override
194 public synchronized void put(String key, Entry entry) {
195 pruneIfNeeded(entry.data.length);
196 File file = getFileForKey(key);
197 try {
198 FileOutputStream fos = new FileOutputStream(file);
199 CacheHeader e = new CacheHeader(key, entry);
200 e.writeHeader(fos);
201 fos.write(entry.data);
202 fos.close();
203 putEntry(key, e);
204 return;
205 } catch (IOException e) {
206 }
207 boolean deleted = file.delete();
208 if (!deleted) {
209 VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
210 }
211 }
212
213 /**
214 * Removes the specified key from the cache if it exists.
215 */
216 @Override
217 public synchronized void remove(String key) {
218 boolean deleted = getFileForKey(key).delete();
219 removeEntry(key);
220 if (!deleted) {
221 VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
222 key, getFilenameForKey(key));
223 }
224 }
225
226 /**
227 * Creates a pseudo-unique filename for the specified cache key.
228 * @param key The key to generate a file name for.
229 * @return A pseudo-unique filename.
230 */
231 private String getFilenameForKey(String key) {
232 int firstHalfLength = key.length() / 2;
233 String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
234 localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
235 return localFilename;
236 }
237
238 /**
239 * Returns a file object for the given cache key.
240 */
241 public File getFileForKey(String key) {
242 return new File(mRootDirectory, getFilenameForKey(key));
243 }
244
245 /**
246 * Prunes the cache to fit the amount of bytes specified.
247 * @param neededSpace The amount of bytes we are trying to fit into the cache.
248 */
249 private void pruneIfNeeded(int neededSpace) {
250 if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
251 return;
252 }
253 if (VolleyLog.DEBUG) {
254 VolleyLog.v("Pruning old cache entries.");
255 }
256
257 long before = mTotalSize;
258 int prunedFiles = 0;
259 long startTime = SystemClock.elapsedRealtime();
260
261 Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
262 while (iterator.hasNext()) {
263 Map.Entry<String, CacheHeader> entry = iterator.next();
264 CacheHeader e = entry.getValue();
265 boolean deleted = getFileForKey(e.key).delete();
266 if (deleted) {
267 mTotalSize -= e.size;
268 } else {
269 VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
270 e.key, getFilenameForKey(e.key));
271 }
272 iterator.remove();
273 prunedFiles++;
274
275 if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
276 break;
277 }
278 }
279
280 if (VolleyLog.DEBUG) {
281 VolleyLog.v("pruned %d files, %d bytes, %d ms",
282 prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
283 }
284 }
285
286 /**
287 * Puts the entry with the specified key into the cache.
288 * @param key The key to identify the entry by.
289 * @param entry The entry to cache.
290 */
291 private void putEntry(String key, CacheHeader entry) {
292 if (!mEntries.containsKey(key)) {
293 mTotalSize += entry.size;
294 } else {
295 CacheHeader oldEntry = mEntries.get(key);
296 mTotalSize += (entry.size - oldEntry.size);
297 }
298 mEntries.put(key, entry);
299 }
300
301 /**
302 * Removes the entry identified by 'key' from the cache.
303 */
304 private void removeEntry(String key) {
305 CacheHeader entry = mEntries.get(key);
306 if (entry != null) {
307 mTotalSize -= entry.size;
308 mEntries.remove(key);
309 }
310 }
311
312 /**
313 * Reads the contents of an InputStream into a byte[].
314 * */
315 private static byte[] streamToBytes(InputStream in, int length) throws IOException {
316 byte[] bytes = new byte[length];
317 int count;
318 int pos = 0;
319 while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
320 pos += count;
321 }
322 if (pos != length) {
323 throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
324 }
325 return bytes;
326 }
327
328 /**
329 * Handles holding onto the cache headers for an entry.
330 */
Ficus Kirkpatricke5a34472013-06-21 09:44:19 -0700331 // Visible for testing.
332 static class CacheHeader {
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800333 /** The size of the data identified by this CacheHeader. (This is not
334 * serialized to disk. */
335 public long size;
336
337 /** The key that identifies the cache entry. */
338 public String key;
339
340 /** ETag for cache coherence. */
341 public String etag;
342
343 /** Date of this response as reported by the server. */
344 public long serverDate;
345
346 /** TTL for this record. */
347 public long ttl;
348
349 /** Soft TTL for this record. */
350 public long softTtl;
351
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800352 /** Headers from the response resulting in this cache entry. */
353 public Map<String, String> responseHeaders;
354
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800355 private CacheHeader() { }
356
357 /**
358 * Instantiates a new CacheHeader object
359 * @param key The key that identifies the cache entry
360 * @param entry The cache entry.
361 */
362 public CacheHeader(String key, Entry entry) {
363 this.key = key;
364 this.size = entry.data.length;
365 this.etag = entry.etag;
366 this.serverDate = entry.serverDate;
367 this.ttl = entry.ttl;
368 this.softTtl = entry.softTtl;
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800369 this.responseHeaders = entry.responseHeaders;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800370 }
371
372 /**
373 * Reads the header off of an InputStream and returns a CacheHeader object.
374 * @param is The InputStream to read from.
375 * @throws IOException
376 */
377 public static CacheHeader readHeader(InputStream is) throws IOException {
378 CacheHeader entry = new CacheHeader();
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700379 int magic = readInt(is);
380 if (magic != CACHE_MAGIC) {
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800381 // don't bother deleting, it'll get pruned eventually
382 throw new IOException();
383 }
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700384 entry.key = readString(is);
385 entry.etag = readString(is);
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800386 if (entry.etag.equals("")) {
387 entry.etag = null;
388 }
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700389 entry.serverDate = readLong(is);
390 entry.ttl = readLong(is);
391 entry.softTtl = readLong(is);
392 entry.responseHeaders = readStringStringMap(is);
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800393 return entry;
394 }
395
396 /**
397 * Creates a cache entry for the specified data.
398 */
399 public Entry toCacheEntry(byte[] data) {
400 Entry e = new Entry();
401 e.data = data;
402 e.etag = etag;
403 e.serverDate = serverDate;
404 e.ttl = ttl;
405 e.softTtl = softTtl;
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800406 e.responseHeaders = responseHeaders;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800407 return e;
408 }
409
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700410
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800411 /**
412 * Writes the contents of this CacheHeader to the specified OutputStream.
413 */
414 public boolean writeHeader(OutputStream os) {
415 try {
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700416 writeInt(os, CACHE_MAGIC);
417 writeString(os, key);
418 writeString(os, etag == null ? "" : etag);
419 writeLong(os, serverDate);
420 writeLong(os, ttl);
421 writeLong(os, softTtl);
422 writeStringStringMap(responseHeaders, os);
423 os.flush();
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800424 return true;
425 } catch (IOException e) {
426 VolleyLog.d("%s", e.toString());
427 return false;
428 }
429 }
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800430
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800431 }
432
433 private static class CountingInputStream extends FilterInputStream {
434 private int bytesRead = 0;
435
436 private CountingInputStream(InputStream in) {
437 super(in);
438 }
439
440 @Override
441 public int read() throws IOException {
442 int result = super.read();
443 if (result != -1) {
444 bytesRead++;
445 }
446 return result;
447 }
448
449 @Override
450 public int read(byte[] buffer, int offset, int count) throws IOException {
451 int result = super.read(buffer, offset, count);
452 if (result != -1) {
453 bytesRead += result;
454 }
455 return result;
456 }
457 }
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700458
459 /*
460 * Homebrewed simple serialization system used for reading and writing cache
461 * headers on disk. Once upon a time, this used the standard Java
462 * Object{Input,Output}Stream, but the default implementation relies heavily
463 * on reflection (even for standard types) and generates a ton of garbage.
464 */
465
466 /**
467 * Simple wrapper around {@link InputStream#read()} that throws EOFException
468 * instead of returning -1.
469 */
470 private static int read(InputStream is) throws IOException {
471 int b = is.read();
472 if (b == -1) {
473 throw new EOFException();
474 }
475 return b;
476 }
477
478 static void writeInt(OutputStream os, int n) throws IOException {
479 os.write((n >> 0) & 0xff);
480 os.write((n >> 8) & 0xff);
481 os.write((n >> 16) & 0xff);
482 os.write((n >> 24) & 0xff);
483 }
484
485 static int readInt(InputStream is) throws IOException {
486 int n = 0;
487 n |= (read(is) << 0);
488 n |= (read(is) << 8);
489 n |= (read(is) << 16);
490 n |= (read(is) << 24);
491 return n;
492 }
493
494 static void writeLong(OutputStream os, long n) throws IOException {
495 os.write((byte)(n >>> 0));
496 os.write((byte)(n >>> 8));
497 os.write((byte)(n >>> 16));
498 os.write((byte)(n >>> 24));
499 os.write((byte)(n >>> 32));
500 os.write((byte)(n >>> 40));
501 os.write((byte)(n >>> 48));
502 os.write((byte)(n >>> 56));
503 }
504
505 static long readLong(InputStream is) throws IOException {
506 long n = 0;
507 n |= ((read(is) & 0xFFL) << 0);
508 n |= ((read(is) & 0xFFL) << 8);
509 n |= ((read(is) & 0xFFL) << 16);
510 n |= ((read(is) & 0xFFL) << 24);
511 n |= ((read(is) & 0xFFL) << 32);
512 n |= ((read(is) & 0xFFL) << 40);
513 n |= ((read(is) & 0xFFL) << 48);
514 n |= ((read(is) & 0xFFL) << 56);
515 return n;
516 }
517
518 static void writeString(OutputStream os, String s) throws IOException {
519 byte[] b = s.getBytes("UTF-8");
520 writeLong(os, b.length);
521 os.write(b, 0, b.length);
522 }
523
524 static String readString(InputStream is) throws IOException {
525 int n = (int) readLong(is);
526 byte[] b = streamToBytes(is, n);
527 return new String(b, "UTF-8");
528 }
529
530 static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
531 if (map != null) {
532 writeInt(os, map.size());
533 for (Map.Entry<String, String> entry : map.entrySet()) {
534 writeString(os, entry.getKey());
535 writeString(os, entry.getValue());
536 }
537 } else {
538 writeInt(os, 0);
539 }
540 }
541
542 static Map<String, String> readStringStringMap(InputStream is) throws IOException {
543 int size = readInt(is);
544 Map<String, String> result = (size == 0)
545 ? Collections.<String, String>emptyMap()
546 : new HashMap<String, String>(size);
547 for (int i = 0; i < size; i++) {
548 String key = readString(is).intern();
549 String value = readString(is).intern();
550 result.put(key, value);
551 }
552 return result;
553 }
554
555
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800556}