• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
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 
17 package com.android.volley.toolbox;
18 
19 import android.os.SystemClock;
20 import android.text.TextUtils;
21 import androidx.annotation.VisibleForTesting;
22 import com.android.volley.Cache;
23 import com.android.volley.Header;
24 import com.android.volley.VolleyLog;
25 import java.io.BufferedInputStream;
26 import java.io.BufferedOutputStream;
27 import java.io.DataInputStream;
28 import java.io.EOFException;
29 import java.io.File;
30 import java.io.FileInputStream;
31 import java.io.FileNotFoundException;
32 import java.io.FileOutputStream;
33 import java.io.FilterInputStream;
34 import java.io.IOException;
35 import java.io.InputStream;
36 import java.io.OutputStream;
37 import java.util.ArrayList;
38 import java.util.Collections;
39 import java.util.Iterator;
40 import java.util.LinkedHashMap;
41 import java.util.List;
42 import java.util.Map;
43 
44 /**
45  * Cache implementation that caches files directly onto the hard disk in the specified directory.
46  * The default disk usage size is 5MB, but is configurable.
47  *
48  * <p>This cache supports the {@link Entry#allResponseHeaders} headers field.
49  */
50 public class DiskBasedCache implements Cache {
51 
52     /** Map of the Key, CacheHeader pairs */
53     private final Map<String, CacheHeader> mEntries = new LinkedHashMap<>(16, .75f, true);
54 
55     /** Total amount of space currently used by the cache in bytes. */
56     private long mTotalSize = 0;
57 
58     /** The supplier for the root directory to use for the cache. */
59     private final FileSupplier mRootDirectorySupplier;
60 
61     /** The maximum size of the cache in bytes. */
62     private final int mMaxCacheSizeInBytes;
63 
64     /** Default maximum disk usage in bytes. */
65     private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
66 
67     /** High water mark percentage for the cache */
68     @VisibleForTesting static final float HYSTERESIS_FACTOR = 0.9f;
69 
70     /** Magic number for current version of cache file format. */
71     private static final int CACHE_MAGIC = 0x20150306;
72 
73     /**
74      * Constructs an instance of the DiskBasedCache at the specified directory.
75      *
76      * @param rootDirectory The root directory of the cache.
77      * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may
78      *     briefly exceed this size on disk when writing a new entry that pushes it over the limit
79      *     until the ensuing pruning completes.
80      */
DiskBasedCache(final File rootDirectory, int maxCacheSizeInBytes)81     public DiskBasedCache(final File rootDirectory, int maxCacheSizeInBytes) {
82         mRootDirectorySupplier =
83                 new FileSupplier() {
84                     @Override
85                     public File get() {
86                         return rootDirectory;
87                     }
88                 };
89         mMaxCacheSizeInBytes = maxCacheSizeInBytes;
90     }
91 
92     /**
93      * Constructs an instance of the DiskBasedCache at the specified directory.
94      *
95      * @param rootDirectorySupplier The supplier for the root directory of the cache.
96      * @param maxCacheSizeInBytes The maximum size of the cache in bytes. Note that the cache may
97      *     briefly exceed this size on disk when writing a new entry that pushes it over the limit
98      *     until the ensuing pruning completes.
99      */
DiskBasedCache(FileSupplier rootDirectorySupplier, int maxCacheSizeInBytes)100     public DiskBasedCache(FileSupplier rootDirectorySupplier, int maxCacheSizeInBytes) {
101         mRootDirectorySupplier = rootDirectorySupplier;
102         mMaxCacheSizeInBytes = maxCacheSizeInBytes;
103     }
104 
105     /**
106      * Constructs an instance of the DiskBasedCache at the specified directory using the default
107      * maximum cache size of 5MB.
108      *
109      * @param rootDirectory The root directory of the cache.
110      */
DiskBasedCache(File rootDirectory)111     public DiskBasedCache(File rootDirectory) {
112         this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
113     }
114 
115     /**
116      * Constructs an instance of the DiskBasedCache at the specified directory using the default
117      * maximum cache size of 5MB.
118      *
119      * @param rootDirectorySupplier The supplier for the root directory of the cache.
120      */
DiskBasedCache(FileSupplier rootDirectorySupplier)121     public DiskBasedCache(FileSupplier rootDirectorySupplier) {
122         this(rootDirectorySupplier, DEFAULT_DISK_USAGE_BYTES);
123     }
124 
125     /** Clears the cache. Deletes all cached files from disk. */
126     @Override
clear()127     public synchronized void clear() {
128         File[] files = mRootDirectorySupplier.get().listFiles();
129         if (files != null) {
130             for (File file : files) {
131                 file.delete();
132             }
133         }
134         mEntries.clear();
135         mTotalSize = 0;
136         VolleyLog.d("Cache cleared.");
137     }
138 
139     /** Returns the cache entry with the specified key if it exists, null otherwise. */
140     @Override
get(String key)141     public synchronized Entry get(String key) {
142         CacheHeader entry = mEntries.get(key);
143         // if the entry does not exist, return.
144         if (entry == null) {
145             return null;
146         }
147         File file = getFileForKey(key);
148         try {
149             CountingInputStream cis =
150                     new CountingInputStream(
151                             new BufferedInputStream(createInputStream(file)), file.length());
152             try {
153                 CacheHeader entryOnDisk = CacheHeader.readHeader(cis);
154                 if (!TextUtils.equals(key, entryOnDisk.key)) {
155                     // File was shared by two keys and now holds data for a different entry!
156                     VolleyLog.d(
157                             "%s: key=%s, found=%s", file.getAbsolutePath(), key, entryOnDisk.key);
158                     // Remove key whose contents on disk have been replaced.
159                     removeEntry(key);
160                     return null;
161                 }
162                 byte[] data = streamToBytes(cis, cis.bytesRemaining());
163                 return entry.toCacheEntry(data);
164             } finally {
165                 // Any IOException thrown here is handled by the below catch block by design.
166                 //noinspection ThrowFromFinallyBlock
167                 cis.close();
168             }
169         } catch (IOException e) {
170             VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
171             remove(key);
172             return null;
173         }
174     }
175 
176     /**
177      * Initializes the DiskBasedCache by scanning for all files currently in the specified root
178      * directory. Creates the root directory if necessary.
179      */
180     @Override
initialize()181     public synchronized void initialize() {
182         File rootDirectory = mRootDirectorySupplier.get();
183         if (!rootDirectory.exists()) {
184             if (!rootDirectory.mkdirs()) {
185                 VolleyLog.e("Unable to create cache dir %s", rootDirectory.getAbsolutePath());
186             }
187             return;
188         }
189         File[] files = rootDirectory.listFiles();
190         if (files == null) {
191             return;
192         }
193         for (File file : files) {
194             try {
195                 long entrySize = file.length();
196                 CountingInputStream cis =
197                         new CountingInputStream(
198                                 new BufferedInputStream(createInputStream(file)), entrySize);
199                 try {
200                     CacheHeader entry = CacheHeader.readHeader(cis);
201                     entry.size = entrySize;
202                     putEntry(entry.key, entry);
203                 } finally {
204                     // Any IOException thrown here is handled by the below catch block by design.
205                     //noinspection ThrowFromFinallyBlock
206                     cis.close();
207                 }
208             } catch (IOException e) {
209                 //noinspection ResultOfMethodCallIgnored
210                 file.delete();
211             }
212         }
213     }
214 
215     /**
216      * Invalidates an entry in the cache.
217      *
218      * @param key Cache key
219      * @param fullExpire True to fully expire the entry, false to soft expire
220      */
221     @Override
invalidate(String key, boolean fullExpire)222     public synchronized void invalidate(String key, boolean fullExpire) {
223         Entry entry = get(key);
224         if (entry != null) {
225             entry.softTtl = 0;
226             if (fullExpire) {
227                 entry.ttl = 0;
228             }
229             put(key, entry);
230         }
231     }
232 
233     /** Puts the entry with the specified key into the cache. */
234     @Override
put(String key, Entry entry)235     public synchronized void put(String key, Entry entry) {
236         // If adding this entry would trigger a prune, but pruning would cause the new entry to be
237         // deleted, then skip writing the entry in the first place, as this is just churn.
238         // Note that we don't include the cache header overhead in this calculation for simplicity,
239         // so putting entries which are just below the threshold may still cause this churn.
240         if (mTotalSize + entry.data.length > mMaxCacheSizeInBytes
241                 && entry.data.length > mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
242             return;
243         }
244         File file = getFileForKey(key);
245         try {
246             BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file));
247             CacheHeader e = new CacheHeader(key, entry);
248             boolean success = e.writeHeader(fos);
249             if (!success) {
250                 fos.close();
251                 VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
252                 throw new IOException();
253             }
254             fos.write(entry.data);
255             fos.close();
256             e.size = file.length();
257             putEntry(key, e);
258             pruneIfNeeded();
259         } catch (IOException e) {
260             boolean deleted = file.delete();
261             if (!deleted) {
262                 VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
263             }
264             initializeIfRootDirectoryDeleted();
265         }
266     }
267 
268     /** Removes the specified key from the cache if it exists. */
269     @Override
remove(String key)270     public synchronized void remove(String key) {
271         boolean deleted = getFileForKey(key).delete();
272         removeEntry(key);
273         if (!deleted) {
274             VolleyLog.d(
275                     "Could not delete cache entry for key=%s, filename=%s",
276                     key, getFilenameForKey(key));
277         }
278     }
279 
280     /**
281      * Creates a pseudo-unique filename for the specified cache key.
282      *
283      * @param key The key to generate a file name for.
284      * @return A pseudo-unique filename.
285      */
getFilenameForKey(String key)286     private String getFilenameForKey(String key) {
287         int firstHalfLength = key.length() / 2;
288         String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
289         localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
290         return localFilename;
291     }
292 
293     /** Returns a file object for the given cache key. */
getFileForKey(String key)294     public File getFileForKey(String key) {
295         return new File(mRootDirectorySupplier.get(), getFilenameForKey(key));
296     }
297 
298     /** Re-initialize the cache if the directory was deleted. */
initializeIfRootDirectoryDeleted()299     private void initializeIfRootDirectoryDeleted() {
300         if (!mRootDirectorySupplier.get().exists()) {
301             VolleyLog.d("Re-initializing cache after external clearing.");
302             mEntries.clear();
303             mTotalSize = 0;
304             initialize();
305         }
306     }
307 
308     /** Represents a supplier for {@link File}s. */
309     public interface FileSupplier {
get()310         File get();
311     }
312 
313     /** Prunes the cache to fit the maximum size. */
pruneIfNeeded()314     private void pruneIfNeeded() {
315         if (mTotalSize < mMaxCacheSizeInBytes) {
316             return;
317         }
318         if (VolleyLog.DEBUG) {
319             VolleyLog.v("Pruning old cache entries.");
320         }
321 
322         long before = mTotalSize;
323         int prunedFiles = 0;
324         long startTime = SystemClock.elapsedRealtime();
325 
326         Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
327         while (iterator.hasNext()) {
328             Map.Entry<String, CacheHeader> entry = iterator.next();
329             CacheHeader e = entry.getValue();
330             boolean deleted = getFileForKey(e.key).delete();
331             if (deleted) {
332                 mTotalSize -= e.size;
333             } else {
334                 VolleyLog.d(
335                         "Could not delete cache entry for key=%s, filename=%s",
336                         e.key, getFilenameForKey(e.key));
337             }
338             iterator.remove();
339             prunedFiles++;
340 
341             if (mTotalSize < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
342                 break;
343             }
344         }
345 
346         if (VolleyLog.DEBUG) {
347             VolleyLog.v(
348                     "pruned %d files, %d bytes, %d ms",
349                     prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
350         }
351     }
352 
353     /**
354      * Puts the entry with the specified key into the cache.
355      *
356      * @param key The key to identify the entry by.
357      * @param entry The entry to cache.
358      */
putEntry(String key, CacheHeader entry)359     private void putEntry(String key, CacheHeader entry) {
360         if (!mEntries.containsKey(key)) {
361             mTotalSize += entry.size;
362         } else {
363             CacheHeader oldEntry = mEntries.get(key);
364             mTotalSize += (entry.size - oldEntry.size);
365         }
366         mEntries.put(key, entry);
367     }
368 
369     /** Removes the entry identified by 'key' from the cache. */
removeEntry(String key)370     private void removeEntry(String key) {
371         CacheHeader removed = mEntries.remove(key);
372         if (removed != null) {
373             mTotalSize -= removed.size;
374         }
375     }
376 
377     /**
378      * Reads length bytes from CountingInputStream into byte array.
379      *
380      * @param cis input stream
381      * @param length number of bytes to read
382      * @throws IOException if fails to read all bytes
383      */
384     @VisibleForTesting
streamToBytes(CountingInputStream cis, long length)385     static byte[] streamToBytes(CountingInputStream cis, long length) throws IOException {
386         long maxLength = cis.bytesRemaining();
387         // Length cannot be negative or greater than bytes remaining, and must not overflow int.
388         if (length < 0 || length > maxLength || (int) length != length) {
389             throw new IOException("streamToBytes length=" + length + ", maxLength=" + maxLength);
390         }
391         byte[] bytes = new byte[(int) length];
392         new DataInputStream(cis).readFully(bytes);
393         return bytes;
394     }
395 
396     @VisibleForTesting
createInputStream(File file)397     InputStream createInputStream(File file) throws FileNotFoundException {
398         return new FileInputStream(file);
399     }
400 
401     @VisibleForTesting
createOutputStream(File file)402     OutputStream createOutputStream(File file) throws FileNotFoundException {
403         return new FileOutputStream(file);
404     }
405 
406     /** Handles holding onto the cache headers for an entry. */
407     @VisibleForTesting
408     static class CacheHeader {
409         /**
410          * The size of the data identified by this CacheHeader on disk (both header and data).
411          *
412          * <p>Must be set by the caller after it has been calculated.
413          *
414          * <p>This is not serialized to disk.
415          */
416         long size;
417 
418         /** The key that identifies the cache entry. */
419         final String key;
420 
421         /** ETag for cache coherence. */
422         final String etag;
423 
424         /** Date of this response as reported by the server. */
425         final long serverDate;
426 
427         /** The last modified date for the requested object. */
428         final long lastModified;
429 
430         /** TTL for this record. */
431         final long ttl;
432 
433         /** Soft TTL for this record. */
434         final long softTtl;
435 
436         /** Headers from the response resulting in this cache entry. */
437         final List<Header> allResponseHeaders;
438 
CacheHeader( String key, String etag, long serverDate, long lastModified, long ttl, long softTtl, List<Header> allResponseHeaders)439         private CacheHeader(
440                 String key,
441                 String etag,
442                 long serverDate,
443                 long lastModified,
444                 long ttl,
445                 long softTtl,
446                 List<Header> allResponseHeaders) {
447             this.key = key;
448             this.etag = "".equals(etag) ? null : etag;
449             this.serverDate = serverDate;
450             this.lastModified = lastModified;
451             this.ttl = ttl;
452             this.softTtl = softTtl;
453             this.allResponseHeaders = allResponseHeaders;
454         }
455 
456         /**
457          * Instantiates a new CacheHeader object.
458          *
459          * @param key The key that identifies the cache entry
460          * @param entry The cache entry.
461          */
CacheHeader(String key, Entry entry)462         CacheHeader(String key, Entry entry) {
463             this(
464                     key,
465                     entry.etag,
466                     entry.serverDate,
467                     entry.lastModified,
468                     entry.ttl,
469                     entry.softTtl,
470                     getAllResponseHeaders(entry));
471         }
472 
getAllResponseHeaders(Entry entry)473         private static List<Header> getAllResponseHeaders(Entry entry) {
474             // If the entry contains all the response headers, use that field directly.
475             if (entry.allResponseHeaders != null) {
476                 return entry.allResponseHeaders;
477             }
478 
479             // Legacy fallback - copy headers from the map.
480             return HttpHeaderParser.toAllHeaderList(entry.responseHeaders);
481         }
482 
483         /**
484          * Reads the header from a CountingInputStream and returns a CacheHeader object.
485          *
486          * @param is The InputStream to read from.
487          * @throws IOException if fails to read header
488          */
readHeader(CountingInputStream is)489         static CacheHeader readHeader(CountingInputStream is) throws IOException {
490             int magic = readInt(is);
491             if (magic != CACHE_MAGIC) {
492                 // don't bother deleting, it'll get pruned eventually
493                 throw new IOException();
494             }
495             String key = readString(is);
496             String etag = readString(is);
497             long serverDate = readLong(is);
498             long lastModified = readLong(is);
499             long ttl = readLong(is);
500             long softTtl = readLong(is);
501             List<Header> allResponseHeaders = readHeaderList(is);
502             return new CacheHeader(
503                     key, etag, serverDate, lastModified, ttl, softTtl, allResponseHeaders);
504         }
505 
506         /** Creates a cache entry for the specified data. */
toCacheEntry(byte[] data)507         Entry toCacheEntry(byte[] data) {
508             Entry e = new Entry();
509             e.data = data;
510             e.etag = etag;
511             e.serverDate = serverDate;
512             e.lastModified = lastModified;
513             e.ttl = ttl;
514             e.softTtl = softTtl;
515             e.responseHeaders = HttpHeaderParser.toHeaderMap(allResponseHeaders);
516             e.allResponseHeaders = Collections.unmodifiableList(allResponseHeaders);
517             return e;
518         }
519 
520         /** Writes the contents of this CacheHeader to the specified OutputStream. */
writeHeader(OutputStream os)521         boolean writeHeader(OutputStream os) {
522             try {
523                 writeInt(os, CACHE_MAGIC);
524                 writeString(os, key);
525                 writeString(os, etag == null ? "" : etag);
526                 writeLong(os, serverDate);
527                 writeLong(os, lastModified);
528                 writeLong(os, ttl);
529                 writeLong(os, softTtl);
530                 writeHeaderList(allResponseHeaders, os);
531                 os.flush();
532                 return true;
533             } catch (IOException e) {
534                 VolleyLog.d("%s", e.toString());
535                 return false;
536             }
537         }
538     }
539 
540     @VisibleForTesting
541     static class CountingInputStream extends FilterInputStream {
542         private final long length;
543         private long bytesRead;
544 
CountingInputStream(InputStream in, long length)545         CountingInputStream(InputStream in, long length) {
546             super(in);
547             this.length = length;
548         }
549 
550         @Override
read()551         public int read() throws IOException {
552             int result = super.read();
553             if (result != -1) {
554                 bytesRead++;
555             }
556             return result;
557         }
558 
559         @Override
read(byte[] buffer, int offset, int count)560         public int read(byte[] buffer, int offset, int count) throws IOException {
561             int result = super.read(buffer, offset, count);
562             if (result != -1) {
563                 bytesRead += result;
564             }
565             return result;
566         }
567 
568         @VisibleForTesting
bytesRead()569         long bytesRead() {
570             return bytesRead;
571         }
572 
bytesRemaining()573         long bytesRemaining() {
574             return length - bytesRead;
575         }
576     }
577 
578     /*
579      * Homebrewed simple serialization system used for reading and writing cache
580      * headers on disk. Once upon a time, this used the standard Java
581      * Object{Input,Output}Stream, but the default implementation relies heavily
582      * on reflection (even for standard types) and generates a ton of garbage.
583      *
584      * TODO: Replace by standard DataInput and DataOutput in next cache version.
585      */
586 
587     /**
588      * Simple wrapper around {@link InputStream#read()} that throws EOFException instead of
589      * returning -1.
590      */
read(InputStream is)591     private static int read(InputStream is) throws IOException {
592         int b = is.read();
593         if (b == -1) {
594             throw new EOFException();
595         }
596         return b;
597     }
598 
writeInt(OutputStream os, int n)599     static void writeInt(OutputStream os, int n) throws IOException {
600         os.write((n >> 0) & 0xff);
601         os.write((n >> 8) & 0xff);
602         os.write((n >> 16) & 0xff);
603         os.write((n >> 24) & 0xff);
604     }
605 
readInt(InputStream is)606     static int readInt(InputStream is) throws IOException {
607         int n = 0;
608         n |= (read(is) << 0);
609         n |= (read(is) << 8);
610         n |= (read(is) << 16);
611         n |= (read(is) << 24);
612         return n;
613     }
614 
writeLong(OutputStream os, long n)615     static void writeLong(OutputStream os, long n) throws IOException {
616         os.write((byte) (n >>> 0));
617         os.write((byte) (n >>> 8));
618         os.write((byte) (n >>> 16));
619         os.write((byte) (n >>> 24));
620         os.write((byte) (n >>> 32));
621         os.write((byte) (n >>> 40));
622         os.write((byte) (n >>> 48));
623         os.write((byte) (n >>> 56));
624     }
625 
readLong(InputStream is)626     static long readLong(InputStream is) throws IOException {
627         long n = 0;
628         n |= ((read(is) & 0xFFL) << 0);
629         n |= ((read(is) & 0xFFL) << 8);
630         n |= ((read(is) & 0xFFL) << 16);
631         n |= ((read(is) & 0xFFL) << 24);
632         n |= ((read(is) & 0xFFL) << 32);
633         n |= ((read(is) & 0xFFL) << 40);
634         n |= ((read(is) & 0xFFL) << 48);
635         n |= ((read(is) & 0xFFL) << 56);
636         return n;
637     }
638 
writeString(OutputStream os, String s)639     static void writeString(OutputStream os, String s) throws IOException {
640         byte[] b = s.getBytes("UTF-8");
641         writeLong(os, b.length);
642         os.write(b, 0, b.length);
643     }
644 
readString(CountingInputStream cis)645     static String readString(CountingInputStream cis) throws IOException {
646         long n = readLong(cis);
647         byte[] b = streamToBytes(cis, n);
648         return new String(b, "UTF-8");
649     }
650 
writeHeaderList(List<Header> headers, OutputStream os)651     static void writeHeaderList(List<Header> headers, OutputStream os) throws IOException {
652         if (headers != null) {
653             writeInt(os, headers.size());
654             for (Header header : headers) {
655                 writeString(os, header.getName());
656                 writeString(os, header.getValue());
657             }
658         } else {
659             writeInt(os, 0);
660         }
661     }
662 
readHeaderList(CountingInputStream cis)663     static List<Header> readHeaderList(CountingInputStream cis) throws IOException {
664         int size = readInt(cis);
665         if (size < 0) {
666             throw new IOException("readHeaderList size=" + size);
667         }
668         List<Header> result =
669                 (size == 0) ? Collections.<Header>emptyList() : new ArrayList<Header>();
670         for (int i = 0; i < size; i++) {
671             String name = readString(cis).intern();
672             String value = readString(cis).intern();
673             result.add(new Header(name, value));
674         }
675         return result;
676     }
677 }
678