blob: d397aad0e1ffe482da118733d4088804b40567c4 [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. */
Aurash Mahbod5bd53252014-06-23 23:03:35 -070064 private static final int CACHE_MAGIC = 0x20140623;
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);
Aurash Mahbod5bd53252014-06-23 23:03:35 -0700200 boolean success = e.writeHeader(fos);
201 if (!success) {
202 fos.close();
203 VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
204 throw new IOException();
205 }
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800206 fos.write(entry.data);
207 fos.close();
208 putEntry(key, e);
209 return;
210 } catch (IOException e) {
211 }
212 boolean deleted = file.delete();
213 if (!deleted) {
214 VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
215 }
216 }
217
218 /**
219 * Removes the specified key from the cache if it exists.
220 */
221 @Override
222 public synchronized void remove(String key) {
223 boolean deleted = getFileForKey(key).delete();
224 removeEntry(key);
225 if (!deleted) {
226 VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
227 key, getFilenameForKey(key));
228 }
229 }
230
231 /**
232 * Creates a pseudo-unique filename for the specified cache key.
233 * @param key The key to generate a file name for.
234 * @return A pseudo-unique filename.
235 */
236 private String getFilenameForKey(String key) {
237 int firstHalfLength = key.length() / 2;
238 String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
239 localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
240 return localFilename;
241 }
242
243 /**
244 * Returns a file object for the given cache key.
245 */
246 public File getFileForKey(String key) {
247 return new File(mRootDirectory, getFilenameForKey(key));
248 }
249
250 /**
251 * Prunes the cache to fit the amount of bytes specified.
252 * @param neededSpace The amount of bytes we are trying to fit into the cache.
253 */
254 private void pruneIfNeeded(int neededSpace) {
255 if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
256 return;
257 }
258 if (VolleyLog.DEBUG) {
259 VolleyLog.v("Pruning old cache entries.");
260 }
261
262 long before = mTotalSize;
263 int prunedFiles = 0;
264 long startTime = SystemClock.elapsedRealtime();
265
266 Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
267 while (iterator.hasNext()) {
268 Map.Entry<String, CacheHeader> entry = iterator.next();
269 CacheHeader e = entry.getValue();
270 boolean deleted = getFileForKey(e.key).delete();
271 if (deleted) {
272 mTotalSize -= e.size;
273 } else {
274 VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
275 e.key, getFilenameForKey(e.key));
276 }
277 iterator.remove();
278 prunedFiles++;
279
280 if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
281 break;
282 }
283 }
284
285 if (VolleyLog.DEBUG) {
286 VolleyLog.v("pruned %d files, %d bytes, %d ms",
287 prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
288 }
289 }
290
291 /**
292 * Puts the entry with the specified key into the cache.
293 * @param key The key to identify the entry by.
294 * @param entry The entry to cache.
295 */
296 private void putEntry(String key, CacheHeader entry) {
297 if (!mEntries.containsKey(key)) {
298 mTotalSize += entry.size;
299 } else {
300 CacheHeader oldEntry = mEntries.get(key);
301 mTotalSize += (entry.size - oldEntry.size);
302 }
303 mEntries.put(key, entry);
304 }
305
306 /**
307 * Removes the entry identified by 'key' from the cache.
308 */
309 private void removeEntry(String key) {
310 CacheHeader entry = mEntries.get(key);
311 if (entry != null) {
312 mTotalSize -= entry.size;
313 mEntries.remove(key);
314 }
315 }
316
317 /**
318 * Reads the contents of an InputStream into a byte[].
319 * */
320 private static byte[] streamToBytes(InputStream in, int length) throws IOException {
321 byte[] bytes = new byte[length];
322 int count;
323 int pos = 0;
324 while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) {
325 pos += count;
326 }
327 if (pos != length) {
328 throw new IOException("Expected " + length + " bytes, read " + pos + " bytes");
329 }
330 return bytes;
331 }
332
333 /**
334 * Handles holding onto the cache headers for an entry.
335 */
Ficus Kirkpatricke5a34472013-06-21 09:44:19 -0700336 // Visible for testing.
337 static class CacheHeader {
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800338 /** The size of the data identified by this CacheHeader. (This is not
339 * serialized to disk. */
340 public long size;
341
342 /** The key that identifies the cache entry. */
343 public String key;
344
345 /** ETag for cache coherence. */
346 public String etag;
347
348 /** Date of this response as reported by the server. */
349 public long serverDate;
350
351 /** TTL for this record. */
352 public long ttl;
353
354 /** Soft TTL for this record. */
355 public long softTtl;
356
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800357 /** Headers from the response resulting in this cache entry. */
358 public Map<String, String> responseHeaders;
359
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800360 private CacheHeader() { }
361
362 /**
363 * Instantiates a new CacheHeader object
364 * @param key The key that identifies the cache entry
365 * @param entry The cache entry.
366 */
367 public CacheHeader(String key, Entry entry) {
368 this.key = key;
369 this.size = entry.data.length;
370 this.etag = entry.etag;
371 this.serverDate = entry.serverDate;
372 this.ttl = entry.ttl;
373 this.softTtl = entry.softTtl;
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800374 this.responseHeaders = entry.responseHeaders;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800375 }
376
377 /**
378 * Reads the header off of an InputStream and returns a CacheHeader object.
379 * @param is The InputStream to read from.
380 * @throws IOException
381 */
382 public static CacheHeader readHeader(InputStream is) throws IOException {
383 CacheHeader entry = new CacheHeader();
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700384 int magic = readInt(is);
385 if (magic != CACHE_MAGIC) {
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800386 // don't bother deleting, it'll get pruned eventually
387 throw new IOException();
388 }
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700389 entry.key = readString(is);
390 entry.etag = readString(is);
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800391 if (entry.etag.equals("")) {
392 entry.etag = null;
393 }
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700394 entry.serverDate = readLong(is);
395 entry.ttl = readLong(is);
396 entry.softTtl = readLong(is);
397 entry.responseHeaders = readStringStringMap(is);
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800398 return entry;
399 }
400
401 /**
402 * Creates a cache entry for the specified data.
403 */
404 public Entry toCacheEntry(byte[] data) {
405 Entry e = new Entry();
406 e.data = data;
407 e.etag = etag;
408 e.serverDate = serverDate;
409 e.ttl = ttl;
410 e.softTtl = softTtl;
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800411 e.responseHeaders = responseHeaders;
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800412 return e;
413 }
414
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700415
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800416 /**
417 * Writes the contents of this CacheHeader to the specified OutputStream.
418 */
419 public boolean writeHeader(OutputStream os) {
420 try {
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700421 writeInt(os, CACHE_MAGIC);
422 writeString(os, key);
423 writeString(os, etag == null ? "" : etag);
424 writeLong(os, serverDate);
425 writeLong(os, ttl);
426 writeLong(os, softTtl);
427 writeStringStringMap(responseHeaders, os);
428 os.flush();
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800429 return true;
430 } catch (IOException e) {
431 VolleyLog.d("%s", e.toString());
432 return false;
433 }
434 }
Jean-Baptiste Querue48f4432012-11-07 07:54:36 -0800435
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800436 }
437
438 private static class CountingInputStream extends FilterInputStream {
439 private int bytesRead = 0;
440
441 private CountingInputStream(InputStream in) {
442 super(in);
443 }
444
445 @Override
446 public int read() throws IOException {
447 int result = super.read();
448 if (result != -1) {
449 bytesRead++;
450 }
451 return result;
452 }
453
454 @Override
455 public int read(byte[] buffer, int offset, int count) throws IOException {
456 int result = super.read(buffer, offset, count);
457 if (result != -1) {
458 bytesRead += result;
459 }
460 return result;
461 }
462 }
Ficus Kirkpatrickb33d0d62013-06-21 10:31:12 -0700463
464 /*
465 * Homebrewed simple serialization system used for reading and writing cache
466 * headers on disk. Once upon a time, this used the standard Java
467 * Object{Input,Output}Stream, but the default implementation relies heavily
468 * on reflection (even for standard types) and generates a ton of garbage.
469 */
470
471 /**
472 * Simple wrapper around {@link InputStream#read()} that throws EOFException
473 * instead of returning -1.
474 */
475 private static int read(InputStream is) throws IOException {
476 int b = is.read();
477 if (b == -1) {
478 throw new EOFException();
479 }
480 return b;
481 }
482
483 static void writeInt(OutputStream os, int n) throws IOException {
484 os.write((n >> 0) & 0xff);
485 os.write((n >> 8) & 0xff);
486 os.write((n >> 16) & 0xff);
487 os.write((n >> 24) & 0xff);
488 }
489
490 static int readInt(InputStream is) throws IOException {
491 int n = 0;
492 n |= (read(is) << 0);
493 n |= (read(is) << 8);
494 n |= (read(is) << 16);
495 n |= (read(is) << 24);
496 return n;
497 }
498
499 static void writeLong(OutputStream os, long n) throws IOException {
500 os.write((byte)(n >>> 0));
501 os.write((byte)(n >>> 8));
502 os.write((byte)(n >>> 16));
503 os.write((byte)(n >>> 24));
504 os.write((byte)(n >>> 32));
505 os.write((byte)(n >>> 40));
506 os.write((byte)(n >>> 48));
507 os.write((byte)(n >>> 56));
508 }
509
510 static long readLong(InputStream is) throws IOException {
511 long n = 0;
512 n |= ((read(is) & 0xFFL) << 0);
513 n |= ((read(is) & 0xFFL) << 8);
514 n |= ((read(is) & 0xFFL) << 16);
515 n |= ((read(is) & 0xFFL) << 24);
516 n |= ((read(is) & 0xFFL) << 32);
517 n |= ((read(is) & 0xFFL) << 40);
518 n |= ((read(is) & 0xFFL) << 48);
519 n |= ((read(is) & 0xFFL) << 56);
520 return n;
521 }
522
523 static void writeString(OutputStream os, String s) throws IOException {
524 byte[] b = s.getBytes("UTF-8");
525 writeLong(os, b.length);
526 os.write(b, 0, b.length);
527 }
528
529 static String readString(InputStream is) throws IOException {
530 int n = (int) readLong(is);
531 byte[] b = streamToBytes(is, n);
532 return new String(b, "UTF-8");
533 }
534
535 static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException {
536 if (map != null) {
537 writeInt(os, map.size());
538 for (Map.Entry<String, String> entry : map.entrySet()) {
539 writeString(os, entry.getKey());
540 writeString(os, entry.getValue());
541 }
542 } else {
543 writeInt(os, 0);
544 }
545 }
546
547 static Map<String, String> readStringStringMap(InputStream is) throws IOException {
548 int size = readInt(is);
549 Map<String, String> result = (size == 0)
550 ? Collections.<String, String>emptyMap()
551 : new HashMap<String, String>(size);
552 for (int i = 0; i < size; i++) {
553 String key = readString(is).intern();
554 String value = readString(is).intern();
555 result.put(key, value);
556 }
557 return result;
558 }
559
560
Jean-Baptiste Querud56b88a2012-11-07 07:48:57 -0800561}