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