1 /* 2 * Copyright (C) 2016 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 package org.mozilla.thirdparty.com.google.android.exoplayer2.upstream.cache; 17 18 import android.annotation.SuppressLint; 19 import android.content.ContentValues; 20 import android.database.Cursor; 21 import android.database.SQLException; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteException; 24 import android.util.SparseArray; 25 import android.util.SparseBooleanArray; 26 import androidx.annotation.Nullable; 27 import androidx.annotation.VisibleForTesting; 28 import androidx.annotation.WorkerThread; 29 import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseIOException; 30 import org.mozilla.thirdparty.com.google.android.exoplayer2.database.DatabaseProvider; 31 import org.mozilla.thirdparty.com.google.android.exoplayer2.database.VersionTable; 32 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Assertions; 33 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.AtomicFile; 34 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.ReusableBufferedOutputStream; 35 import org.mozilla.thirdparty.com.google.android.exoplayer2.util.Util; 36 import java.io.BufferedInputStream; 37 import java.io.ByteArrayInputStream; 38 import java.io.ByteArrayOutputStream; 39 import java.io.DataInputStream; 40 import java.io.DataOutputStream; 41 import java.io.File; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.io.OutputStream; 45 import java.security.InvalidAlgorithmParameterException; 46 import java.security.InvalidKeyException; 47 import java.security.NoSuchAlgorithmException; 48 import java.util.Arrays; 49 import java.util.Collection; 50 import java.util.HashMap; 51 import java.util.Map; 52 import java.util.Random; 53 import java.util.Set; 54 import javax.crypto.Cipher; 55 import javax.crypto.CipherInputStream; 56 import javax.crypto.CipherOutputStream; 57 import javax.crypto.NoSuchPaddingException; 58 import javax.crypto.spec.IvParameterSpec; 59 import javax.crypto.spec.SecretKeySpec; 60 import org.checkerframework.checker.nullness.compatqual.NullableType; 61 62 /** Maintains the index of cached content. */ 63 /* package */ class CachedContentIndex { 64 65 /* package */ static final String FILE_NAME_ATOMIC = "cached_content_index.exi"; 66 67 private static final int INCREMENTAL_METADATA_READ_LENGTH = 10 * 1024 * 1024; 68 69 private final HashMap<String, CachedContent> keyToContent; 70 /** 71 * Maps assigned ids to their corresponding keys. Also contains (id -> null) entries for ids that 72 * have been removed from the index since it was last stored. This prevents reuse of these ids, 73 * which is necessary to avoid clashes that could otherwise occur as a result of the sequence: 74 * 75 * <p>[1] (key1, id1) is removed from the in-memory index ... the index is not stored to disk ... 76 * [2] id1 is reused for a different key2 ... the index is not stored to disk ... [3] A file for 77 * key2 is partially written using a path corresponding to id1 ... the process is killed before 78 * the index is stored to disk ... [4] The index is read from disk, causing the partially written 79 * file to be incorrectly associated to key1 80 * 81 * <p>By avoiding id reuse in step [2], a new id2 will be used instead. Step [4] will then delete 82 * the partially written file because the index does not contain an entry for id2. 83 * 84 * <p>When the index is next stored (id -> null) entries are removed, making the ids eligible for 85 * reuse. 86 */ 87 private final SparseArray<@NullableType String> idToKey; 88 /** 89 * Tracks ids for which (id -> null) entries are present in idToKey, so that they can be removed 90 * efficiently when the index is next stored. 91 */ 92 private final SparseBooleanArray removedIds; 93 /** Tracks ids that are new since the index was last stored. */ 94 private final SparseBooleanArray newIds; 95 96 private Storage storage; 97 @Nullable private Storage previousStorage; 98 99 /** Returns whether the file is an index file. */ isIndexFile(String fileName)100 public static boolean isIndexFile(String fileName) { 101 // Atomic file backups add additional suffixes to the file name. 102 return fileName.startsWith(FILE_NAME_ATOMIC); 103 } 104 105 /** 106 * Deletes index data for the specified cache. 107 * 108 * <p>This method may be slow and shouldn't normally be called on the main thread. 109 * 110 * @param databaseProvider Provides the database in which the index is stored. 111 * @param uid The cache UID. 112 * @throws DatabaseIOException If an error occurs deleting the index data. 113 */ 114 @WorkerThread delete(DatabaseProvider databaseProvider, long uid)115 public static void delete(DatabaseProvider databaseProvider, long uid) 116 throws DatabaseIOException { 117 DatabaseStorage.delete(databaseProvider, uid); 118 } 119 120 /** 121 * Creates an instance supporting database storage only. 122 * 123 * @param databaseProvider Provides the database in which the index is stored. 124 */ CachedContentIndex(DatabaseProvider databaseProvider)125 public CachedContentIndex(DatabaseProvider databaseProvider) { 126 this( 127 databaseProvider, 128 /* legacyStorageDir= */ null, 129 /* legacyStorageSecretKey= */ null, 130 /* legacyStorageEncrypt= */ false, 131 /* preferLegacyStorage= */ false); 132 } 133 134 /** 135 * Creates an instance supporting either or both of database and legacy storage. 136 * 137 * @param databaseProvider Provides the database in which the index is stored, or {@code null} to 138 * use only legacy storage. 139 * @param legacyStorageDir The directory in which any legacy storage is stored, or {@code null} to 140 * use only database storage. 141 * @param legacyStorageSecretKey A 16 byte AES key for reading, and optionally writing, legacy 142 * storage. 143 * @param legacyStorageEncrypt Whether to encrypt when writing to legacy storage. Must be false if 144 * {@code legacyStorageSecretKey} is null. 145 * @param preferLegacyStorage Whether to use prefer legacy storage if both storage types are 146 * enabled. This option is only useful for downgrading from database storage back to legacy 147 * storage. 148 */ CachedContentIndex( @ullable DatabaseProvider databaseProvider, @Nullable File legacyStorageDir, @Nullable byte[] legacyStorageSecretKey, boolean legacyStorageEncrypt, boolean preferLegacyStorage)149 public CachedContentIndex( 150 @Nullable DatabaseProvider databaseProvider, 151 @Nullable File legacyStorageDir, 152 @Nullable byte[] legacyStorageSecretKey, 153 boolean legacyStorageEncrypt, 154 boolean preferLegacyStorage) { 155 Assertions.checkState(databaseProvider != null || legacyStorageDir != null); 156 keyToContent = new HashMap<>(); 157 idToKey = new SparseArray<>(); 158 removedIds = new SparseBooleanArray(); 159 newIds = new SparseBooleanArray(); 160 Storage databaseStorage = 161 databaseProvider != null ? new DatabaseStorage(databaseProvider) : null; 162 Storage legacyStorage = 163 legacyStorageDir != null 164 ? new LegacyStorage( 165 new File(legacyStorageDir, FILE_NAME_ATOMIC), 166 legacyStorageSecretKey, 167 legacyStorageEncrypt) 168 : null; 169 if (databaseStorage == null || (legacyStorage != null && preferLegacyStorage)) { 170 storage = legacyStorage; 171 previousStorage = databaseStorage; 172 } else { 173 storage = databaseStorage; 174 previousStorage = legacyStorage; 175 } 176 } 177 178 /** 179 * Loads the index data for the given cache UID. 180 * 181 * <p>This method may be slow and shouldn't normally be called on the main thread. 182 * 183 * @param uid The UID of the cache whose index is to be loaded. 184 * @throws IOException If an error occurs initializing the index data. 185 */ 186 @WorkerThread initialize(long uid)187 public void initialize(long uid) throws IOException { 188 storage.initialize(uid); 189 if (previousStorage != null) { 190 previousStorage.initialize(uid); 191 } 192 if (!storage.exists() && previousStorage != null && previousStorage.exists()) { 193 // Copy from previous storage into current storage. 194 previousStorage.load(keyToContent, idToKey); 195 storage.storeFully(keyToContent); 196 } else { 197 // Load from the current storage. 198 storage.load(keyToContent, idToKey); 199 } 200 if (previousStorage != null) { 201 previousStorage.delete(); 202 previousStorage = null; 203 } 204 } 205 206 /** 207 * Stores the index data to index file if there is a change. 208 * 209 * <p>This method may be slow and shouldn't normally be called on the main thread. 210 * 211 * @throws IOException If an error occurs storing the index data. 212 */ 213 @WorkerThread store()214 public void store() throws IOException { 215 storage.storeIncremental(keyToContent); 216 // Make ids that were removed since the index was last stored eligible for re-use. 217 int removedIdCount = removedIds.size(); 218 for (int i = 0; i < removedIdCount; i++) { 219 idToKey.remove(removedIds.keyAt(i)); 220 } 221 removedIds.clear(); 222 newIds.clear(); 223 } 224 225 /** 226 * Adds the given key to the index if it isn't there already. 227 * 228 * @param key The cache key that uniquely identifies the original stream. 229 * @return A new or existing CachedContent instance with the given key. 230 */ getOrAdd(String key)231 public CachedContent getOrAdd(String key) { 232 CachedContent cachedContent = keyToContent.get(key); 233 return cachedContent == null ? addNew(key) : cachedContent; 234 } 235 236 /** Returns a CachedContent instance with the given key or null if there isn't one. */ get(String key)237 public CachedContent get(String key) { 238 return keyToContent.get(key); 239 } 240 241 /** 242 * Returns a Collection of all CachedContent instances in the index. The collection is backed by 243 * the {@code keyToContent} map, so changes to the map are reflected in the collection, and 244 * vice-versa. If the map is modified while an iteration over the collection is in progress 245 * (except through the iterator's own remove operation), the results of the iteration are 246 * undefined. 247 */ getAll()248 public Collection<CachedContent> getAll() { 249 return keyToContent.values(); 250 } 251 252 /** Returns an existing or new id assigned to the given key. */ assignIdForKey(String key)253 public int assignIdForKey(String key) { 254 return getOrAdd(key).id; 255 } 256 257 /** Returns the key which has the given id assigned. */ getKeyForId(int id)258 public String getKeyForId(int id) { 259 return idToKey.get(id); 260 } 261 262 /** Removes {@link CachedContent} with the given key from index if it's empty and not locked. */ maybeRemove(String key)263 public void maybeRemove(String key) { 264 CachedContent cachedContent = keyToContent.get(key); 265 if (cachedContent != null && cachedContent.isEmpty() && !cachedContent.isLocked()) { 266 keyToContent.remove(key); 267 int id = cachedContent.id; 268 boolean neverStored = newIds.get(id); 269 storage.onRemove(cachedContent, neverStored); 270 if (neverStored) { 271 // The id can be reused immediately. 272 idToKey.remove(id); 273 newIds.delete(id); 274 } else { 275 // Keep an entry in idToKey to stop the id from being reused until the index is next stored, 276 // and add an entry to removedIds to track that it should be removed when this does happen. 277 idToKey.put(id, /* value= */ null); 278 removedIds.put(id, /* value= */ true); 279 } 280 } 281 } 282 283 /** Removes empty and not locked {@link CachedContent} instances from index. */ removeEmpty()284 public void removeEmpty() { 285 String[] keys = new String[keyToContent.size()]; 286 keyToContent.keySet().toArray(keys); 287 for (String key : keys) { 288 maybeRemove(key); 289 } 290 } 291 292 /** 293 * Returns a set of all content keys. The set is backed by the {@code keyToContent} map, so 294 * changes to the map are reflected in the set, and vice-versa. If the map is modified while an 295 * iteration over the set is in progress (except through the iterator's own remove operation), the 296 * results of the iteration are undefined. 297 */ getKeys()298 public Set<String> getKeys() { 299 return keyToContent.keySet(); 300 } 301 302 /** 303 * Applies {@code mutations} to the {@link ContentMetadata} for the given key. A new {@link 304 * CachedContent} is added if there isn't one already with the given key. 305 */ applyContentMetadataMutations(String key, ContentMetadataMutations mutations)306 public void applyContentMetadataMutations(String key, ContentMetadataMutations mutations) { 307 CachedContent cachedContent = getOrAdd(key); 308 if (cachedContent.applyMetadataMutations(mutations)) { 309 storage.onUpdate(cachedContent); 310 } 311 } 312 313 /** Returns a {@link ContentMetadata} for the given key. */ getContentMetadata(String key)314 public ContentMetadata getContentMetadata(String key) { 315 CachedContent cachedContent = get(key); 316 return cachedContent != null ? cachedContent.getMetadata() : DefaultContentMetadata.EMPTY; 317 } 318 addNew(String key)319 private CachedContent addNew(String key) { 320 int id = getNewId(idToKey); 321 CachedContent cachedContent = new CachedContent(id, key); 322 keyToContent.put(key, cachedContent); 323 idToKey.put(id, key); 324 newIds.put(id, true); 325 storage.onUpdate(cachedContent); 326 return cachedContent; 327 } 328 329 @SuppressLint("GetInstance") // Suppress warning about specifying "BC" as an explicit provider. getCipher()330 private static Cipher getCipher() throws NoSuchPaddingException, NoSuchAlgorithmException { 331 // Workaround for https://issuetracker.google.com/issues/36976726 332 if (Util.SDK_INT == 18) { 333 try { 334 return Cipher.getInstance("AES/CBC/PKCS5PADDING", "BC"); 335 } catch (Throwable ignored) { 336 // ignored 337 } 338 } 339 return Cipher.getInstance("AES/CBC/PKCS5PADDING"); 340 } 341 342 /** 343 * Returns an id which isn't used in the given array. If the maximum id in the array is smaller 344 * than {@link java.lang.Integer#MAX_VALUE} it just returns the next bigger integer. Otherwise it 345 * returns the smallest unused non-negative integer. 346 */ 347 @VisibleForTesting getNewId(SparseArray<String> idToKey)348 /* package */ static int getNewId(SparseArray<String> idToKey) { 349 int size = idToKey.size(); 350 int id = size == 0 ? 0 : (idToKey.keyAt(size - 1) + 1); 351 if (id < 0) { // In case if we pass max int value. 352 // TODO optimization: defragmentation or binary search? 353 for (id = 0; id < size; id++) { 354 if (id != idToKey.keyAt(id)) { 355 break; 356 } 357 } 358 } 359 return id; 360 } 361 362 /** 363 * Deserializes a {@link DefaultContentMetadata} from the given input stream. 364 * 365 * @param input Input stream to read from. 366 * @return a {@link DefaultContentMetadata} instance. 367 * @throws IOException If an error occurs during reading from the input. 368 */ readContentMetadata(DataInputStream input)369 private static DefaultContentMetadata readContentMetadata(DataInputStream input) 370 throws IOException { 371 int size = input.readInt(); 372 HashMap<String, byte[]> metadata = new HashMap<>(); 373 for (int i = 0; i < size; i++) { 374 String name = input.readUTF(); 375 int valueSize = input.readInt(); 376 if (valueSize < 0) { 377 throw new IOException("Invalid value size: " + valueSize); 378 } 379 // Grow the array incrementally to avoid OutOfMemoryError in the case that a corrupt (and very 380 // large) valueSize was read. In such cases the implementation below is expected to throw 381 // IOException from one of the readFully calls, due to the end of the input being reached. 382 int bytesRead = 0; 383 int nextBytesToRead = Math.min(valueSize, INCREMENTAL_METADATA_READ_LENGTH); 384 byte[] value = Util.EMPTY_BYTE_ARRAY; 385 while (bytesRead != valueSize) { 386 value = Arrays.copyOf(value, bytesRead + nextBytesToRead); 387 input.readFully(value, bytesRead, nextBytesToRead); 388 bytesRead += nextBytesToRead; 389 nextBytesToRead = Math.min(valueSize - bytesRead, INCREMENTAL_METADATA_READ_LENGTH); 390 } 391 metadata.put(name, value); 392 } 393 return new DefaultContentMetadata(metadata); 394 } 395 396 /** 397 * Serializes itself to a {@link DataOutputStream}. 398 * 399 * @param output Output stream to store the values. 400 * @throws IOException If an error occurs writing to the output. 401 */ writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output)402 private static void writeContentMetadata(DefaultContentMetadata metadata, DataOutputStream output) 403 throws IOException { 404 Set<Map.Entry<String, byte[]>> entrySet = metadata.entrySet(); 405 output.writeInt(entrySet.size()); 406 for (Map.Entry<String, byte[]> entry : entrySet) { 407 output.writeUTF(entry.getKey()); 408 byte[] value = entry.getValue(); 409 output.writeInt(value.length); 410 output.write(value); 411 } 412 } 413 414 /** Interface for the persistent index. */ 415 private interface Storage { 416 417 /** Initializes the storage for the given cache UID. */ initialize(long uid)418 void initialize(long uid); 419 420 /** 421 * Returns whether the persisted index exists. 422 * 423 * @throws IOException If an error occurs determining whether the persisted index exists. 424 */ exists()425 boolean exists() throws IOException; 426 427 /** 428 * Deletes the persisted index. 429 * 430 * @throws IOException If an error occurs deleting the index. 431 */ delete()432 void delete() throws IOException; 433 434 /** 435 * Loads the persisted index into {@code content} and {@code idToKey}, creating it if it doesn't 436 * already exist. 437 * 438 * <p>If the persisted index is in a permanently bad state (i.e. all further attempts to load it 439 * are also expected to fail) then it will be deleted and the call will return successfully. For 440 * transient failures, {@link IOException} will be thrown. 441 * 442 * @param content The key to content map to populate with persisted data. 443 * @param idToKey The id to key map to populate with persisted data. 444 * @throws IOException If an error occurs loading the index. 445 */ load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)446 void load(HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) 447 throws IOException; 448 449 /** 450 * Writes the persisted index, creating it if it doesn't already exist and replacing any 451 * existing content if it does. 452 * 453 * @param content The key to content map to persist. 454 * @throws IOException If an error occurs persisting the index. 455 */ storeFully(HashMap<String, CachedContent> content)456 void storeFully(HashMap<String, CachedContent> content) throws IOException; 457 458 /** 459 * Ensures incremental changes to the index since the initial {@link #initialize(long)} or last 460 * {@link #storeFully(HashMap)} are persisted. The storage will have been notified of all such 461 * changes via {@link #onUpdate(CachedContent)} and {@link #onRemove(CachedContent, boolean)}. 462 * 463 * @param content The key to content map to persist. 464 * @throws IOException If an error occurs persisting the index. 465 */ storeIncremental(HashMap<String, CachedContent> content)466 void storeIncremental(HashMap<String, CachedContent> content) throws IOException; 467 468 /** 469 * Called when a {@link CachedContent} is added or updated. 470 * 471 * @param cachedContent The updated {@link CachedContent}. 472 */ onUpdate(CachedContent cachedContent)473 void onUpdate(CachedContent cachedContent); 474 475 /** 476 * Called when a {@link CachedContent} is removed. 477 * 478 * @param cachedContent The removed {@link CachedContent}. 479 * @param neverStored True if the {@link CachedContent} was added more recently than when the 480 * index was last stored. 481 */ onRemove(CachedContent cachedContent, boolean neverStored)482 void onRemove(CachedContent cachedContent, boolean neverStored); 483 } 484 485 /** {@link Storage} implementation that uses an {@link AtomicFile}. */ 486 private static class LegacyStorage implements Storage { 487 488 private static final int VERSION = 2; 489 private static final int VERSION_METADATA_INTRODUCED = 2; 490 private static final int FLAG_ENCRYPTED_INDEX = 1; 491 492 private final boolean encrypt; 493 @Nullable private final Cipher cipher; 494 @Nullable private final SecretKeySpec secretKeySpec; 495 @Nullable private final Random random; 496 private final AtomicFile atomicFile; 497 498 private boolean changed; 499 @Nullable private ReusableBufferedOutputStream bufferedOutputStream; 500 LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt)501 public LegacyStorage(File file, @Nullable byte[] secretKey, boolean encrypt) { 502 Cipher cipher = null; 503 SecretKeySpec secretKeySpec = null; 504 if (secretKey != null) { 505 Assertions.checkArgument(secretKey.length == 16); 506 try { 507 cipher = getCipher(); 508 secretKeySpec = new SecretKeySpec(secretKey, "AES"); 509 } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { 510 throw new IllegalStateException(e); // Should never happen. 511 } 512 } else { 513 Assertions.checkArgument(!encrypt); 514 } 515 this.encrypt = encrypt; 516 this.cipher = cipher; 517 this.secretKeySpec = secretKeySpec; 518 random = encrypt ? new Random() : null; 519 atomicFile = new AtomicFile(file); 520 } 521 522 @Override initialize(long uid)523 public void initialize(long uid) { 524 // Do nothing. Legacy storage uses a separate file for each cache. 525 } 526 527 @Override exists()528 public boolean exists() { 529 return atomicFile.exists(); 530 } 531 532 @Override delete()533 public void delete() { 534 atomicFile.delete(); 535 } 536 537 @Override load( HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)538 public void load( 539 HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) { 540 Assertions.checkState(!changed); 541 if (!readFile(content, idToKey)) { 542 content.clear(); 543 idToKey.clear(); 544 atomicFile.delete(); 545 } 546 } 547 548 @Override storeFully(HashMap<String, CachedContent> content)549 public void storeFully(HashMap<String, CachedContent> content) throws IOException { 550 writeFile(content); 551 changed = false; 552 } 553 554 @Override storeIncremental(HashMap<String, CachedContent> content)555 public void storeIncremental(HashMap<String, CachedContent> content) throws IOException { 556 if (!changed) { 557 return; 558 } 559 storeFully(content); 560 } 561 562 @Override onUpdate(CachedContent cachedContent)563 public void onUpdate(CachedContent cachedContent) { 564 changed = true; 565 } 566 567 @Override onRemove(CachedContent cachedContent, boolean neverStored)568 public void onRemove(CachedContent cachedContent, boolean neverStored) { 569 changed = true; 570 } 571 readFile( HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)572 private boolean readFile( 573 HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) { 574 if (!atomicFile.exists()) { 575 return true; 576 } 577 578 DataInputStream input = null; 579 try { 580 InputStream inputStream = new BufferedInputStream(atomicFile.openRead()); 581 input = new DataInputStream(inputStream); 582 int version = input.readInt(); 583 if (version < 0 || version > VERSION) { 584 return false; 585 } 586 587 int flags = input.readInt(); 588 if ((flags & FLAG_ENCRYPTED_INDEX) != 0) { 589 if (cipher == null) { 590 return false; 591 } 592 byte[] initializationVector = new byte[16]; 593 input.readFully(initializationVector); 594 IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); 595 try { 596 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); 597 } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { 598 throw new IllegalStateException(e); 599 } 600 input = new DataInputStream(new CipherInputStream(inputStream, cipher)); 601 } else if (encrypt) { 602 changed = true; // Force index to be rewritten encrypted after read. 603 } 604 605 int count = input.readInt(); 606 int hashCode = 0; 607 for (int i = 0; i < count; i++) { 608 CachedContent cachedContent = readCachedContent(version, input); 609 content.put(cachedContent.key, cachedContent); 610 idToKey.put(cachedContent.id, cachedContent.key); 611 hashCode += hashCachedContent(cachedContent, version); 612 } 613 int fileHashCode = input.readInt(); 614 boolean isEOF = input.read() == -1; 615 if (fileHashCode != hashCode || !isEOF) { 616 return false; 617 } 618 } catch (IOException e) { 619 return false; 620 } finally { 621 if (input != null) { 622 Util.closeQuietly(input); 623 } 624 } 625 return true; 626 } 627 writeFile(HashMap<String, CachedContent> content)628 private void writeFile(HashMap<String, CachedContent> content) throws IOException { 629 DataOutputStream output = null; 630 try { 631 OutputStream outputStream = atomicFile.startWrite(); 632 if (bufferedOutputStream == null) { 633 bufferedOutputStream = new ReusableBufferedOutputStream(outputStream); 634 } else { 635 bufferedOutputStream.reset(outputStream); 636 } 637 output = new DataOutputStream(bufferedOutputStream); 638 output.writeInt(VERSION); 639 640 int flags = encrypt ? FLAG_ENCRYPTED_INDEX : 0; 641 output.writeInt(flags); 642 643 if (encrypt) { 644 byte[] initializationVector = new byte[16]; 645 random.nextBytes(initializationVector); 646 output.write(initializationVector); 647 IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector); 648 try { 649 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); 650 } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { 651 throw new IllegalStateException(e); // Should never happen. 652 } 653 output.flush(); 654 output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher)); 655 } 656 657 output.writeInt(content.size()); 658 int hashCode = 0; 659 for (CachedContent cachedContent : content.values()) { 660 writeCachedContent(cachedContent, output); 661 hashCode += hashCachedContent(cachedContent, VERSION); 662 } 663 output.writeInt(hashCode); 664 atomicFile.endWrite(output); 665 // Avoid calling close twice. Duplicate CipherOutputStream.close calls did 666 // not used to be no-ops: https://android-review.googlesource.com/#/c/272799/ 667 output = null; 668 } finally { 669 Util.closeQuietly(output); 670 } 671 } 672 673 /** 674 * Calculates a hash code for a {@link CachedContent} which is compatible with a particular 675 * index version. 676 */ hashCachedContent(CachedContent cachedContent, int version)677 private int hashCachedContent(CachedContent cachedContent, int version) { 678 int result = cachedContent.id; 679 result = 31 * result + cachedContent.key.hashCode(); 680 if (version < VERSION_METADATA_INTRODUCED) { 681 long length = ContentMetadata.getContentLength(cachedContent.getMetadata()); 682 result = 31 * result + (int) (length ^ (length >>> 32)); 683 } else { 684 result = 31 * result + cachedContent.getMetadata().hashCode(); 685 } 686 return result; 687 } 688 689 /** 690 * Reads a {@link CachedContent} from a {@link DataInputStream}. 691 * 692 * @param version Version of the encoded data. 693 * @param input Input stream containing values needed to initialize CachedContent instance. 694 * @throws IOException If an error occurs during reading values. 695 */ readCachedContent(int version, DataInputStream input)696 private CachedContent readCachedContent(int version, DataInputStream input) throws IOException { 697 int id = input.readInt(); 698 String key = input.readUTF(); 699 DefaultContentMetadata metadata; 700 if (version < VERSION_METADATA_INTRODUCED) { 701 long length = input.readLong(); 702 ContentMetadataMutations mutations = new ContentMetadataMutations(); 703 ContentMetadataMutations.setContentLength(mutations, length); 704 metadata = DefaultContentMetadata.EMPTY.copyWithMutationsApplied(mutations); 705 } else { 706 metadata = readContentMetadata(input); 707 } 708 return new CachedContent(id, key, metadata); 709 } 710 711 /** 712 * Writes a {@link CachedContent} to a {@link DataOutputStream}. 713 * 714 * @param output Output stream to store the values. 715 * @throws IOException If an error occurs during writing values to output. 716 */ writeCachedContent(CachedContent cachedContent, DataOutputStream output)717 private void writeCachedContent(CachedContent cachedContent, DataOutputStream output) 718 throws IOException { 719 output.writeInt(cachedContent.id); 720 output.writeUTF(cachedContent.key); 721 writeContentMetadata(cachedContent.getMetadata(), output); 722 } 723 } 724 725 /** {@link Storage} implementation that uses an SQL database. */ 726 private static final class DatabaseStorage implements Storage { 727 728 private static final String TABLE_PREFIX = DatabaseProvider.TABLE_PREFIX + "CacheIndex"; 729 private static final int TABLE_VERSION = 1; 730 731 private static final String COLUMN_ID = "id"; 732 private static final String COLUMN_KEY = "key"; 733 private static final String COLUMN_METADATA = "metadata"; 734 735 private static final int COLUMN_INDEX_ID = 0; 736 private static final int COLUMN_INDEX_KEY = 1; 737 private static final int COLUMN_INDEX_METADATA = 2; 738 739 private static final String WHERE_ID_EQUALS = COLUMN_ID + " = ?"; 740 741 private static final String[] COLUMNS = new String[] {COLUMN_ID, COLUMN_KEY, COLUMN_METADATA}; 742 private static final String TABLE_SCHEMA = 743 "(" 744 + COLUMN_ID 745 + " INTEGER PRIMARY KEY NOT NULL," 746 + COLUMN_KEY 747 + " TEXT NOT NULL," 748 + COLUMN_METADATA 749 + " BLOB NOT NULL)"; 750 751 private final DatabaseProvider databaseProvider; 752 private final SparseArray<CachedContent> pendingUpdates; 753 754 private String hexUid; 755 private String tableName; 756 delete(DatabaseProvider databaseProvider, long uid)757 public static void delete(DatabaseProvider databaseProvider, long uid) 758 throws DatabaseIOException { 759 delete(databaseProvider, Long.toHexString(uid)); 760 } 761 DatabaseStorage(DatabaseProvider databaseProvider)762 public DatabaseStorage(DatabaseProvider databaseProvider) { 763 this.databaseProvider = databaseProvider; 764 pendingUpdates = new SparseArray<>(); 765 } 766 767 @Override initialize(long uid)768 public void initialize(long uid) { 769 hexUid = Long.toHexString(uid); 770 tableName = getTableName(hexUid); 771 } 772 773 @Override exists()774 public boolean exists() throws DatabaseIOException { 775 return VersionTable.getVersion( 776 databaseProvider.getReadableDatabase(), 777 VersionTable.FEATURE_CACHE_CONTENT_METADATA, 778 hexUid) 779 != VersionTable.VERSION_UNSET; 780 } 781 782 @Override delete()783 public void delete() throws DatabaseIOException { 784 delete(databaseProvider, hexUid); 785 } 786 787 @Override load( HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey)788 public void load( 789 HashMap<String, CachedContent> content, SparseArray<@NullableType String> idToKey) 790 throws IOException { 791 Assertions.checkState(pendingUpdates.size() == 0); 792 try { 793 int version = 794 VersionTable.getVersion( 795 databaseProvider.getReadableDatabase(), 796 VersionTable.FEATURE_CACHE_CONTENT_METADATA, 797 hexUid); 798 if (version != TABLE_VERSION) { 799 SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); 800 writableDatabase.beginTransactionNonExclusive(); 801 try { 802 initializeTable(writableDatabase); 803 writableDatabase.setTransactionSuccessful(); 804 } finally { 805 writableDatabase.endTransaction(); 806 } 807 } 808 809 try (Cursor cursor = getCursor()) { 810 while (cursor.moveToNext()) { 811 int id = cursor.getInt(COLUMN_INDEX_ID); 812 String key = cursor.getString(COLUMN_INDEX_KEY); 813 byte[] metadataBytes = cursor.getBlob(COLUMN_INDEX_METADATA); 814 815 ByteArrayInputStream inputStream = new ByteArrayInputStream(metadataBytes); 816 DataInputStream input = new DataInputStream(inputStream); 817 DefaultContentMetadata metadata = readContentMetadata(input); 818 819 CachedContent cachedContent = new CachedContent(id, key, metadata); 820 content.put(cachedContent.key, cachedContent); 821 idToKey.put(cachedContent.id, cachedContent.key); 822 } 823 } 824 } catch (SQLiteException e) { 825 content.clear(); 826 idToKey.clear(); 827 throw new DatabaseIOException(e); 828 } 829 } 830 831 @Override storeFully(HashMap<String, CachedContent> content)832 public void storeFully(HashMap<String, CachedContent> content) throws IOException { 833 try { 834 SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); 835 writableDatabase.beginTransactionNonExclusive(); 836 try { 837 initializeTable(writableDatabase); 838 for (CachedContent cachedContent : content.values()) { 839 addOrUpdateRow(writableDatabase, cachedContent); 840 } 841 writableDatabase.setTransactionSuccessful(); 842 pendingUpdates.clear(); 843 } finally { 844 writableDatabase.endTransaction(); 845 } 846 } catch (SQLException e) { 847 throw new DatabaseIOException(e); 848 } 849 } 850 851 @Override storeIncremental(HashMap<String, CachedContent> content)852 public void storeIncremental(HashMap<String, CachedContent> content) throws IOException { 853 if (pendingUpdates.size() == 0) { 854 return; 855 } 856 try { 857 SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); 858 writableDatabase.beginTransactionNonExclusive(); 859 try { 860 for (int i = 0; i < pendingUpdates.size(); i++) { 861 CachedContent cachedContent = pendingUpdates.valueAt(i); 862 if (cachedContent == null) { 863 deleteRow(writableDatabase, pendingUpdates.keyAt(i)); 864 } else { 865 addOrUpdateRow(writableDatabase, cachedContent); 866 } 867 } 868 writableDatabase.setTransactionSuccessful(); 869 pendingUpdates.clear(); 870 } finally { 871 writableDatabase.endTransaction(); 872 } 873 } catch (SQLException e) { 874 throw new DatabaseIOException(e); 875 } 876 } 877 878 @Override onUpdate(CachedContent cachedContent)879 public void onUpdate(CachedContent cachedContent) { 880 pendingUpdates.put(cachedContent.id, cachedContent); 881 } 882 883 @Override onRemove(CachedContent cachedContent, boolean neverStored)884 public void onRemove(CachedContent cachedContent, boolean neverStored) { 885 if (neverStored) { 886 pendingUpdates.delete(cachedContent.id); 887 } else { 888 pendingUpdates.put(cachedContent.id, null); 889 } 890 } 891 getCursor()892 private Cursor getCursor() { 893 return databaseProvider 894 .getReadableDatabase() 895 .query( 896 tableName, 897 COLUMNS, 898 /* selection= */ null, 899 /* selectionArgs= */ null, 900 /* groupBy= */ null, 901 /* having= */ null, 902 /* orderBy= */ null); 903 } 904 initializeTable(SQLiteDatabase writableDatabase)905 private void initializeTable(SQLiteDatabase writableDatabase) throws DatabaseIOException { 906 VersionTable.setVersion( 907 writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid, TABLE_VERSION); 908 dropTable(writableDatabase, tableName); 909 writableDatabase.execSQL("CREATE TABLE " + tableName + " " + TABLE_SCHEMA); 910 } 911 deleteRow(SQLiteDatabase writableDatabase, int key)912 private void deleteRow(SQLiteDatabase writableDatabase, int key) { 913 writableDatabase.delete(tableName, WHERE_ID_EQUALS, new String[] {Integer.toString(key)}); 914 } 915 addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent)916 private void addOrUpdateRow(SQLiteDatabase writableDatabase, CachedContent cachedContent) 917 throws IOException { 918 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 919 writeContentMetadata(cachedContent.getMetadata(), new DataOutputStream(outputStream)); 920 byte[] data = outputStream.toByteArray(); 921 922 ContentValues values = new ContentValues(); 923 values.put(COLUMN_ID, cachedContent.id); 924 values.put(COLUMN_KEY, cachedContent.key); 925 values.put(COLUMN_METADATA, data); 926 writableDatabase.replaceOrThrow(tableName, /* nullColumnHack= */ null, values); 927 } 928 delete(DatabaseProvider databaseProvider, String hexUid)929 private static void delete(DatabaseProvider databaseProvider, String hexUid) 930 throws DatabaseIOException { 931 try { 932 String tableName = getTableName(hexUid); 933 SQLiteDatabase writableDatabase = databaseProvider.getWritableDatabase(); 934 writableDatabase.beginTransactionNonExclusive(); 935 try { 936 VersionTable.removeVersion( 937 writableDatabase, VersionTable.FEATURE_CACHE_CONTENT_METADATA, hexUid); 938 dropTable(writableDatabase, tableName); 939 writableDatabase.setTransactionSuccessful(); 940 } finally { 941 writableDatabase.endTransaction(); 942 } 943 } catch (SQLException e) { 944 throw new DatabaseIOException(e); 945 } 946 } 947 dropTable(SQLiteDatabase writableDatabase, String tableName)948 private static void dropTable(SQLiteDatabase writableDatabase, String tableName) { 949 writableDatabase.execSQL("DROP TABLE IF EXISTS " + tableName); 950 } 951 getTableName(String hexUid)952 private static String getTableName(String hexUid) { 953 return TABLE_PREFIX + hexUid; 954 } 955 } 956 } 957