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 17package com.android.volley.toolbox; 18 19import android.os.SystemClock; 20 21import com.android.volley.Cache; 22import com.android.volley.VolleyLog; 23 24import java.io.EOFException; 25import java.io.File; 26import java.io.FileInputStream; 27import java.io.FileOutputStream; 28import java.io.FilterInputStream; 29import java.io.IOException; 30import java.io.InputStream; 31import java.io.OutputStream; 32import java.util.Collections; 33import java.util.HashMap; 34import 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 63 /** Magic number for current version of cache file format. */ 64 private static final int CACHE_MAGIC = 0x20120504; 65 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 136 * specified root directory. Creates the root directory if necessary. 137 */ 138 @Override 139 public synchronized void initialize() { 140 if (!mRootDirectory.exists()) { 141 if (!mRootDirectory.mkdirs()) { 142 VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath()); 143 } 144 return; 145 } 146 147 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 */ 331 // Visible for testing. 332 static class CacheHeader { 333 /** 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 352 /** Headers from the response resulting in this cache entry. */ 353 public Map<String, String> responseHeaders; 354 355 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; 369 this.responseHeaders = entry.responseHeaders; 370 } 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(); 379 int magic = readInt(is); 380 if (magic != CACHE_MAGIC) { 381 // don't bother deleting, it'll get pruned eventually 382 throw new IOException(); 383 } 384 entry.key = readString(is); 385 entry.etag = readString(is); 386 if (entry.etag.equals("")) { 387 entry.etag = null; 388 } 389 entry.serverDate = readLong(is); 390 entry.ttl = readLong(is); 391 entry.softTtl = readLong(is); 392 entry.responseHeaders = readStringStringMap(is); 393 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; 406 e.responseHeaders = responseHeaders; 407 return e; 408 } 409 410 411 /** 412 * Writes the contents of this CacheHeader to the specified OutputStream. 413 */ 414 public boolean writeHeader(OutputStream os) { 415 try { 416 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(); 424 return true; 425 } catch (IOException e) { 426 VolleyLog.d("%s", e.toString()); 427 return false; 428 } 429 } 430 431 } 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 } 458 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 556} 557