1 /* 2 * Copyright (C) 2022 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.adservices.shared.storage; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ATOMIC_FILE_DATASTORE_READ_FAILURE; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ATOMIC_FILE_DATASTORE_WRITE_FAILURE; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; 22 import static com.android.adservices.shared.util.LogUtil.DEBUG; 23 import static com.android.adservices.shared.util.LogUtil.VERBOSE; 24 import static com.android.adservices.shared.util.Preconditions.checkState; 25 import static com.android.internal.util.Preconditions.checkArgumentNonnegative; 26 27 import android.annotation.Nullable; 28 import android.os.PersistableBundle; 29 import android.util.AtomicFile; 30 31 import com.android.adservices.shared.errorlogging.AdServicesErrorLogger; 32 import com.android.adservices.shared.util.LogUtil; 33 import com.android.internal.annotations.GuardedBy; 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.modules.utils.build.SdkLevel; 36 37 import java.io.ByteArrayInputStream; 38 import java.io.ByteArrayOutputStream; 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 import java.io.FileOutputStream; 42 import java.io.IOException; 43 import java.io.PrintWriter; 44 import java.util.HashMap; 45 import java.util.HashSet; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.Set; 49 import java.util.concurrent.locks.Lock; 50 import java.util.concurrent.locks.ReadWriteLock; 51 import java.util.concurrent.locks.ReentrantReadWriteLock; 52 import java.util.stream.Collectors; 53 54 /** 55 * A simple datastore utilizing {@link android.util.AtomicFile} and {@link 56 * android.os.PersistableBundle} to read/write a simple key/value map to file. 57 * 58 * <p>The datastore is loaded from file only when initialized and written to file on every write. 59 * When using this datastore, it is up to the caller to ensure that each datastore file is accessed 60 * by exactly one datastore object. If multiple writing threads or processes attempt to use 61 * different instances pointing to the same file, transactions may be lost. 62 * 63 * <p>Keys must be non-{@code null}, non-empty strings, and values can be booleans, integers or 64 * strings. 65 * 66 * @threadsafe 67 */ 68 public final class AtomicFileDatastore { 69 public static final int NO_PREVIOUS_VERSION = -1; 70 71 /** 72 * Argument to {@link #dump(PrintWriter, String, String[])} so it includes the database content. 73 */ 74 public static final String DUMP_ARG_INCLUDE_CONTENTS = "--include_contents"; 75 76 /** Convenience reference to dump args that only contains {@link #DUMP_ARG_INCLUDE_CONTENTS}. */ 77 public static final String[] DUMP_ARGS_INCLUDE_CONTENTS_ONLY = 78 new String[] {DUMP_ARG_INCLUDE_CONTENTS}; 79 80 private final int mDatastoreVersion; 81 private final AdServicesErrorLogger mAdServicesErrorLogger; 82 83 private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(); 84 private final Lock mReadLock = mReadWriteLock.readLock(); 85 private final Lock mWriteLock = mReadWriteLock.writeLock(); 86 87 private final AtomicFile mAtomicFile; 88 private final Map<String, Object> mLocalMap = new HashMap<>(); 89 90 private final String mVersionKey; 91 private int mPreviousStoredVersion; 92 AtomicFileDatastore( File file, int datastoreVersion, String versionKey, AdServicesErrorLogger adServicesErrorLogger)93 public AtomicFileDatastore( 94 File file, 95 int datastoreVersion, 96 String versionKey, 97 AdServicesErrorLogger adServicesErrorLogger) { 98 this(new AtomicFile(validFile(file)), datastoreVersion, versionKey, adServicesErrorLogger); 99 } 100 101 @VisibleForTesting // AtomicFileDatastoreTest must spy on AtomicFile AtomicFileDatastore( AtomicFile atomicFile, int datastoreVersion, String versionKey, AdServicesErrorLogger adServicesErrorLogger)102 AtomicFileDatastore( 103 AtomicFile atomicFile, 104 int datastoreVersion, 105 String versionKey, 106 AdServicesErrorLogger adServicesErrorLogger) { 107 mAtomicFile = Objects.requireNonNull(atomicFile, "atomicFile cannot be null"); 108 mDatastoreVersion = 109 checkArgumentNonnegative(datastoreVersion, "datastoreVersion must not be negative"); 110 111 mVersionKey = checkValid("versionKey", versionKey); 112 mAdServicesErrorLogger = 113 Objects.requireNonNull( 114 adServicesErrorLogger, "adServicesErrorLogger cannot be null"); 115 } 116 117 /** 118 * Loads data from the datastore file. 119 * 120 * @throws IOException if file read fails 121 */ initialize()122 public void initialize() throws IOException { 123 if (DEBUG) { 124 LogUtil.d("Reading from store file: %s", mAtomicFile.getBaseFile()); 125 } 126 mReadLock.lock(); 127 try { 128 readFromFile(); 129 } finally { 130 mReadLock.unlock(); 131 } 132 133 // In the future, this could be a good place for upgrade/rollback for schemas 134 } 135 136 // Writes the {@code localMap} to a PersistableBundle which is then written to file. 137 @GuardedBy("mWriteLock") writeToFile(Map<String, Object> localMap)138 private void writeToFile(Map<String, Object> localMap) throws IOException { 139 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 140 PersistableBundle bundleToWrite = new PersistableBundle(); 141 142 for (Map.Entry<String, Object> entry : localMap.entrySet()) { 143 addToBundle(bundleToWrite, entry.getKey(), entry.getValue()); 144 } 145 146 // Version unused for now. May be needed in the future for handling migrations. 147 bundleToWrite.putInt(mVersionKey, mDatastoreVersion); 148 bundleToWrite.writeToStream(outputStream); 149 150 FileOutputStream out = null; 151 try { 152 out = mAtomicFile.startWrite(); 153 out.write(outputStream.toByteArray()); 154 mAtomicFile.finishWrite(out); 155 } catch (IOException e) { 156 if (out != null) { 157 mAtomicFile.failWrite(out); 158 } 159 LogUtil.v("Write to file %s failed", mAtomicFile.getBaseFile()); 160 mAdServicesErrorLogger.logError( 161 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ATOMIC_FILE_DATASTORE_WRITE_FAILURE, 162 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 163 throw e; 164 } 165 } 166 167 // Note that this completely replaces the loaded datastore with the file's data, instead of 168 // appending new file data. 169 @GuardedBy("mReadLock") readFromFile()170 private void readFromFile() throws IOException { 171 try { 172 final ByteArrayInputStream inputStream = 173 new ByteArrayInputStream(mAtomicFile.readFully()); 174 final PersistableBundle bundleRead = PersistableBundle.readFromStream(inputStream); 175 176 mPreviousStoredVersion = bundleRead.getInt(mVersionKey, NO_PREVIOUS_VERSION); 177 bundleRead.remove(mVersionKey); 178 mLocalMap.clear(); 179 for (String key : bundleRead.keySet()) { 180 mLocalMap.put(key, bundleRead.get(key)); 181 } 182 } catch (FileNotFoundException e) { 183 if (VERBOSE) { 184 LogUtil.v("File not found; continuing with clear database"); 185 } 186 mPreviousStoredVersion = NO_PREVIOUS_VERSION; 187 mLocalMap.clear(); 188 } catch (IOException e) { 189 LogUtil.v("Read from store file %s failed", mAtomicFile.getBaseFile()); 190 mAdServicesErrorLogger.logError( 191 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ATOMIC_FILE_DATASTORE_READ_FAILURE, 192 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 193 throw e; 194 } 195 } 196 197 /** 198 * Stores a boolean value to the datastore file. 199 * 200 * <p>This change is committed immediately to file. 201 * 202 * @param key A non-null, non-empty String key to store the {@code value} against 203 * @param value A boolean to be stored 204 * @throws IllegalArgumentException if {@code key} is an empty string 205 * @throws IOException if file write fails 206 */ putBoolean(String key, boolean value)207 public void putBoolean(String key, boolean value) throws IOException { 208 put(key, value); 209 } 210 211 /** 212 * Stores an integer value to the datastore file. 213 * 214 * <p>This change is committed immediately to file. 215 * 216 * @param key A non-null, non-empty String key to store the {@code value} against 217 * @param value An integer to be stored 218 * @throws IllegalArgumentException if {@code key} is an empty string 219 * @throws IOException if file write fails 220 */ putInt(String key, int value)221 public void putInt(String key, int value) throws IOException { 222 put(key, value); 223 } 224 225 /** 226 * Stores a string value to the datastore file. 227 * 228 * <p>This change is committed immediately to file. 229 * 230 * @param key A non-null, non-empty String key to store the {@code value} against 231 * @param value A string to be stored 232 * @throws IllegalArgumentException if {@code key} is an empty string 233 * @throws IOException if file write fails 234 */ putString(String key, @Nullable String value)235 public void putString(String key, @Nullable String value) throws IOException { 236 put(key, value); 237 } 238 put(String key, @Nullable Object value)239 private void put(String key, @Nullable Object value) throws IOException { 240 checkValidKey(key); 241 242 mWriteLock.lock(); 243 Object oldValue = mLocalMap.get(key); 244 try { 245 mLocalMap.put(key, value); 246 writeToFile(mLocalMap); 247 } catch (IOException ex) { 248 LogUtil.v( 249 "put(): failed to write to file %s, reverting value of %s on local map.", 250 mAtomicFile.getBaseFile(), key); 251 if (oldValue == null) { 252 mLocalMap.remove(key); 253 } else { 254 mLocalMap.put(key, oldValue); 255 } 256 throw ex; 257 } finally { 258 mWriteLock.unlock(); 259 } 260 } 261 262 /** 263 * Stores a boolean value to the datastore file, but only if the key does not already exist. 264 * 265 * <p>If a change is made to the datastore, it is committed immediately to file. 266 * 267 * @param key A non-null, non-empty String key to store the {@code value} against 268 * @param value A boolean to be stored 269 * @return the value that exists in the datastore after the operation completes 270 * @throws IllegalArgumentException if {@code key} is an empty string 271 * @throws IOException if file write fails 272 */ putBooleanIfNew(String key, boolean value)273 public boolean putBooleanIfNew(String key, boolean value) throws IOException { 274 return putIfNew(key, value, Boolean.class); 275 } 276 277 /** 278 * Stores an integer value to the datastore file, but only if the key does not already exist. 279 * 280 * <p>If a change is made to the datastore, it is committed immediately to file. 281 * 282 * @param key A non-null, non-empty String key to store the {@code value} against 283 * @param value An integer to be stored 284 * @return the value that exists in the datastore after the operation completes 285 * @throws IllegalArgumentException if {@code key} is an empty string 286 * @throws IOException if file write fails 287 */ putIntIfNew(String key, int value)288 public int putIntIfNew(String key, int value) throws IOException { 289 return putIfNew(key, value, Integer.class); 290 } 291 292 /** 293 * Stores a String value to the datastore file, but only if the key does not already exist. 294 * 295 * <p>If a change is made to the datastore, it is committed immediately to file. 296 * 297 * @param key A non-null, non-empty String key to store the {@code value} against 298 * @param value A String to be stored 299 * @return the value that exists in the datastore after the operation completes 300 * @throws IllegalArgumentException if {@code key} is an empty string 301 * @throws IOException if file write fails 302 */ putStringIfNew(String key, String value)303 public String putStringIfNew(String key, String value) throws IOException { 304 return putIfNew(key, value, String.class); 305 } 306 putIfNew(String key, T value, Class<T> valueType)307 private <T> T putIfNew(String key, T value, Class<T> valueType) throws IOException { 308 checkValidKey(key); 309 Objects.requireNonNull(valueType, "valueType cannot be null"); 310 311 // Try not to block readers first before trying to write 312 mReadLock.lock(); 313 try { 314 Object valueInLocalMap = mLocalMap.get(key); 315 if (valueInLocalMap != null) { 316 return checkValueType(valueInLocalMap, valueType); 317 } 318 319 } finally { 320 mReadLock.unlock(); 321 } 322 323 // Double check that the key wasn't written after the first check 324 mWriteLock.lock(); 325 Object valueInLocalMap = mLocalMap.get(key); 326 try { 327 if (valueInLocalMap != null) { 328 return checkValueType(valueInLocalMap, valueType); 329 } else { 330 mLocalMap.put(key, value); 331 writeToFile(mLocalMap); 332 return value; 333 } 334 } catch (IOException ex) { 335 LogUtil.v( 336 "putIfNew(): failed to write to file %s, removing key %s on local map.", 337 mAtomicFile.getBaseFile(), key); 338 mLocalMap.remove(key); 339 throw ex; 340 } finally { 341 mWriteLock.unlock(); 342 } 343 } 344 345 /** 346 * Retrieves a boolean value from the loaded datastore file. 347 * 348 * @param key A non-null, non-empty String key to fetch a value from 349 * @return The value stored against a {@code key}, or null if it doesn't exist 350 * @throws IllegalArgumentException if {@code key} is an empty string 351 */ 352 @Nullable getBoolean(String key)353 public Boolean getBoolean(String key) { 354 return get(key, Boolean.class); 355 } 356 357 /** 358 * Retrieves an integer value from the loaded datastore file. 359 * 360 * @param key A non-null, non-empty String key to fetch a value from 361 * @return The value stored against a {@code key}, or null if it doesn't exist 362 * @throws IllegalArgumentException if {@code key} is an empty string 363 */ 364 @Nullable getInt(String key)365 public Integer getInt(String key) { 366 return get(key, Integer.class); 367 } 368 369 /** 370 * Retrieves a String value from the loaded datastore file. 371 * 372 * @param key A non-null, non-empty String key to fetch a value from 373 * @return The value stored against a {@code key}, or null if it doesn't exist 374 * @throws IllegalArgumentException if {@code key} is an empty string 375 */ 376 @Nullable getString(String key)377 public String getString(String key) { 378 return get(key, String.class); 379 } 380 381 @Nullable get(String key, Class<T> valueType)382 private <T> T get(String key, Class<T> valueType) { 383 checkValidKey(key); 384 385 mReadLock.lock(); 386 try { 387 if (mLocalMap.containsKey(key)) { 388 Object valueInLocalMap = mLocalMap.get(key); 389 return checkValueType(valueInLocalMap, valueType); 390 } 391 return null; 392 } finally { 393 mReadLock.unlock(); 394 } 395 } 396 397 /** 398 * Retrieves a boolean value from the loaded datastore file. 399 * 400 * @param key A non-null, non-empty String key to fetch a value from 401 * @param defaultValue Value to return if this key does not exist. 402 * @return The value stored against a {@code key}, or {@code defaultValue} if it doesn't exist 403 * @throws IllegalArgumentException if {@code key} is an empty string 404 */ getBoolean(String key, boolean defaultValue)405 public boolean getBoolean(String key, boolean defaultValue) { 406 return get(key, defaultValue, Boolean.class); 407 } 408 409 /** 410 * Retrieves an integer value from the loaded datastore file. 411 * 412 * @param key A non-null, non-empty String key to fetch a value from 413 * @param defaultValue Value to return if this key does not exist. 414 * @return The value stored against a {@code key}, or {@code defaultValue} if it doesn't exist 415 * @throws IllegalArgumentException if {@code key} is an empty string 416 */ getInt(String key, int defaultValue)417 public int getInt(String key, int defaultValue) { 418 return get(key, defaultValue, Integer.class); 419 } 420 421 /** 422 * Retrieves a String value from the loaded datastore file. 423 * 424 * @param key A non-null, non-empty String key to fetch a value from 425 * @param defaultValue Value to return if this key does not exist. 426 * @return The value stored against a {@code key}, or {@code defaultValue} if it doesn't exist 427 * @throws IllegalArgumentException if {@code key} is an empty string 428 */ getString(String key, String defaultValue)429 public String getString(String key, String defaultValue) { 430 return get(key, defaultValue, String.class); 431 } 432 get(String key, T defaultValue, Class<T> valueType)433 private <T> T get(String key, T defaultValue, Class<T> valueType) { 434 checkValidKey(key); 435 Objects.requireNonNull(defaultValue, "Default value must not be null"); 436 437 mReadLock.lock(); 438 try { 439 if (mLocalMap.containsKey(key)) { 440 Object valueInLocalMap = mLocalMap.get(key); 441 return checkValueType(valueInLocalMap, valueType); 442 } 443 return defaultValue; 444 } finally { 445 mReadLock.unlock(); 446 } 447 } 448 449 /** 450 * Retrieves a {@link Set} of all keys loaded from the datastore file. 451 * 452 * @return A {@link Set} of {@link String} keys currently in the loaded datastore 453 */ keySet()454 public Set<String> keySet() { 455 mReadLock.lock(); 456 try { 457 return getSafeSetCopy(mLocalMap.keySet()); 458 } finally { 459 mReadLock.unlock(); 460 } 461 } 462 keySetFilter(Object filter)463 private Set<String> keySetFilter(Object filter) { 464 mReadLock.lock(); 465 try { 466 return getSafeSetCopy( 467 mLocalMap.entrySet().stream() 468 .filter(entry -> entry.getValue().equals(filter)) 469 .map(Map.Entry::getKey) 470 .collect(Collectors.toSet())); 471 } finally { 472 mReadLock.unlock(); 473 } 474 } 475 476 /** 477 * Retrieves a Set of all keys with value {@code true} loaded from the datastore file. 478 * 479 * @return A Set of String keys currently in the loaded datastore that have value {@code true} 480 */ keySetTrue()481 public Set<String> keySetTrue() { 482 return keySetFilter(true); 483 } 484 485 /** 486 * Retrieves a Set of all keys with value {@code false} loaded from the datastore file. 487 * 488 * @return A Set of String keys currently in the loaded datastore that have value {@code false} 489 */ keySetFalse()490 public Set<String> keySetFalse() { 491 return keySetFilter(false); 492 } 493 494 /** 495 * Clears all entries from the datastore file. 496 * 497 * <p>This change is committed immediately to file. 498 * 499 * @throws IOException if file write fails 500 */ clear()501 public void clear() throws IOException { 502 mWriteLock.lock(); 503 if (DEBUG) { 504 LogUtil.d( 505 "Clearing all (%d) entries from datastore (%s)", 506 mLocalMap.size(), mAtomicFile.getBaseFile()); 507 } 508 Map<String, Object> previousLocalMap = new HashMap<>(mLocalMap); 509 try { 510 mLocalMap.clear(); 511 writeToFile(mLocalMap); 512 } catch (IOException ex) { 513 LogUtil.v( 514 "clear(): failed to clear the file %s, reverting local map back to previous " 515 + "state.", 516 mAtomicFile.getBaseFile()); 517 mLocalMap.putAll(previousLocalMap); 518 throw ex; 519 } finally { 520 mWriteLock.unlock(); 521 } 522 } 523 clearByFilter(Object filter)524 private void clearByFilter(Object filter) throws IOException { 525 mWriteLock.lock(); 526 Map<String, Object> previousLocalMap = new HashMap<>(mLocalMap); 527 try { 528 mLocalMap.entrySet().removeIf(entry -> entry.getValue().equals(filter)); 529 writeToFile(mLocalMap); 530 } catch (IOException ex) { 531 LogUtil.v( 532 "clearByFilter(): failed to clear keys for filter %s for file %s, reverting" 533 + " local map back to previous state.", 534 filter, mAtomicFile.getBaseFile()); 535 mLocalMap.putAll(previousLocalMap); 536 throw ex; 537 } finally { 538 mWriteLock.unlock(); 539 } 540 } 541 542 /** 543 * Clears all entries from the datastore file that have value {@code true}. Entries with value 544 * {@code false} are not removed. 545 * 546 * <p>This change is committed immediately to file. 547 * 548 * @throws IOException if file write fails 549 */ clearAllTrue()550 public void clearAllTrue() throws IOException { 551 clearByFilter(true); 552 } 553 554 /** 555 * Clears all entries from the datastore file that have value {@code false}. Entries with value 556 * {@code true} are not removed. 557 * 558 * <p>This change is committed immediately to file. 559 * 560 * @throws IOException if file write fails 561 */ clearAllFalse()562 public void clearAllFalse() throws IOException { 563 clearByFilter(false); 564 } 565 566 /** 567 * Removes an entry from the datastore file. 568 * 569 * <p>This change is committed immediately to file. 570 * 571 * @param key A non-null, non-empty String key to remove 572 * @throws IllegalArgumentException if {@code key} is an empty string 573 * @throws IOException if file write fails 574 */ remove(String key)575 public void remove(String key) throws IOException { 576 checkValidKey(key); 577 578 mWriteLock.lock(); 579 Object oldValue = mLocalMap.get(key); 580 try { 581 mLocalMap.remove(key); 582 writeToFile(mLocalMap); 583 } catch (IOException ex) { 584 LogUtil.v( 585 "remove(): failed to remove key %s in file %s, adding it back", 586 key, mAtomicFile.getBaseFile()); 587 if (oldValue != null) { 588 mLocalMap.put(key, oldValue); 589 } 590 throw ex; 591 } finally { 592 mWriteLock.unlock(); 593 } 594 } 595 596 /** 597 * Removes all entries that begin with the specified prefix from the datastore file. 598 * 599 * <p>This change is committed immediately to file. 600 * 601 * @param prefix A non-null, non-empty string that all keys are matched against 602 * @throws IllegalArgumentException if {@code prefix} is an empty string 603 * @throws IOException if file write fails 604 */ removeByPrefix(String prefix)605 public void removeByPrefix(String prefix) throws IOException { 606 checkValid("prefix", prefix); 607 608 mWriteLock.lock(); 609 Map<String, Object> previousLocalMap = new HashMap<>(mLocalMap); 610 try { 611 Set<String> allKeys = mLocalMap.keySet(); 612 Set<String> keysToDelete = 613 allKeys.stream().filter(s -> s.startsWith(prefix)).collect(Collectors.toSet()); 614 allKeys.removeAll(keysToDelete); // Modifying the keySet updates the underlying map 615 writeToFile(mLocalMap); 616 } catch (IOException ex) { 617 LogUtil.v( 618 "removeByPrefix(): failed to remove key by prefix %s in file %s, adding it" 619 + " back", 620 prefix, mAtomicFile.getBaseFile()); 621 mLocalMap.putAll(previousLocalMap); 622 throw ex; 623 } finally { 624 mWriteLock.unlock(); 625 } 626 } 627 628 /** 629 * Updates the file and local map by applying the {@code transform} to the most recently 630 * persisted value. 631 * 632 * @param transform The {@link BatchUpdateOperation} to apply to the data. 633 * @throws IOException if file write fails 634 */ update(BatchUpdateOperation transform)635 public void update(BatchUpdateOperation transform) throws IOException { 636 mWriteLock.lock(); 637 try { 638 BatchUpdaterImpl updater = new BatchUpdaterImpl(mLocalMap); 639 transform.apply(updater); 640 641 // Write to file if contents in map are changed 642 if (updater.isChanged()) { 643 writeToFile(updater.mUpdatedCachedData); 644 mLocalMap.clear(); 645 mLocalMap.putAll(updater.mUpdatedCachedData); 646 } 647 } finally { 648 mWriteLock.unlock(); 649 } 650 } 651 652 /** Dumps its internal state. */ dump(PrintWriter writer, String prefix, @Nullable String[] args)653 public void dump(PrintWriter writer, String prefix, @Nullable String[] args) { 654 writer.printf("%smDatastoreVersion: %d\n", prefix, mDatastoreVersion); 655 writer.printf("%smPreviousStoredVersion: %d\n", prefix, mPreviousStoredVersion); 656 writer.printf("%smVersionKey: %s\n", prefix, mVersionKey); 657 writer.printf("%smAtomicFile: %s", prefix, mAtomicFile.getBaseFile().getAbsolutePath()); 658 if (SdkLevel.isAtLeastS()) { 659 writer.printf(" (last modified at %d)\n", mAtomicFile.getLastModifiedTime()); 660 } 661 662 boolean dumpAll = args != null && args[0].equals(DUMP_ARG_INCLUDE_CONTENTS); 663 int size = mLocalMap.size(); 664 writer.printf("%s%d entries", prefix, size); 665 if (!dumpAll || size == 0) { 666 writer.println(); 667 return; 668 } 669 writer.println(":"); 670 String prefix2 = prefix + prefix; 671 mLocalMap.forEach((k, v) -> writer.printf("%s%s: %s\n", prefix2, k, v)); 672 } 673 674 /** Returns the version that was written prior to the device starting. */ getPreviousStoredVersion()675 public int getPreviousStoredVersion() { 676 return mPreviousStoredVersion; 677 } 678 679 /** Gets the version key. */ getVersionKey()680 public String getVersionKey() { 681 return mVersionKey; 682 } 683 684 @Override toString()685 public String toString() { 686 StringBuilder string = 687 new StringBuilder("AtomicFileDatastore[path=") 688 .append(mAtomicFile.getBaseFile().getAbsolutePath()) 689 .append(", version=") 690 .append(mDatastoreVersion) 691 .append(", previousVersion=") 692 .append(mPreviousStoredVersion) 693 .append(", versionKey=") 694 .append(mVersionKey) 695 .append(", entries="); 696 mReadLock.lock(); 697 try { 698 string.append(mLocalMap.size()); 699 } finally { 700 mReadLock.unlock(); 701 } 702 return string.append(']').toString(); 703 } 704 705 /** 706 * Helper method to support various data types. 707 * 708 * <p>Equivalent to calling {@link android.os.BaseBundle#putObject(String, Object)}, which is 709 * hidden. 710 */ addToBundle(PersistableBundle bundle, String key, Object value)711 private void addToBundle(PersistableBundle bundle, String key, Object value) { 712 Objects.requireNonNull(key, "cannot add null key"); 713 Objects.requireNonNull(value, "cannot add null value for key " + key); 714 715 if (value instanceof Boolean) { 716 bundle.putBoolean(key, (Boolean) value); 717 } else if (value instanceof Integer) { 718 bundle.putInt(key, (Integer) value); 719 } else if (value instanceof String) { 720 bundle.putString(key, (String) value); 721 } else { 722 throw new IllegalArgumentException( 723 "Failed to insert unsupported type: " 724 + value.getClass() 725 + " value for key: " 726 + key); 727 } 728 } 729 validFile(File file)730 private static File validFile(File file) { 731 Objects.requireNonNull(file, "file cannot be null"); 732 File parent = file.getParentFile(); 733 if (!parent.exists()) { 734 throw new IllegalArgumentException( 735 "parentPath doesn't exist: " + parent.getAbsolutePath()); 736 } 737 return file; 738 } 739 740 // TODO(b/335869310): change it to using ImmutableSet. getSafeSetCopy(Set<T> sourceSet)741 private static <T> Set<T> getSafeSetCopy(Set<T> sourceSet) { 742 return new HashSet<>(sourceSet); 743 } 744 checkValidKey(String key)745 private static void checkValidKey(String key) { 746 checkValid("key", key); 747 } 748 checkValid(String what, String value)749 private static String checkValid(String what, String value) { 750 if (value == null) { 751 throw new NullPointerException(what + " must not be null"); 752 } 753 if (value.isEmpty()) { 754 throw new IllegalArgumentException(what + " must not be empty"); 755 } 756 return value; 757 } 758 checkValueType(Object valueInLocalMap, Class<T> expectedType)759 private static <T> T checkValueType(Object valueInLocalMap, Class<T> expectedType) { 760 checkState( 761 expectedType.isInstance(valueInLocalMap), 762 "Value returned is not of %s type", 763 expectedType.getSimpleName()); 764 return expectedType.cast(valueInLocalMap); 765 } 766 767 /** A functional interface to perform batch operations on the datastore */ 768 @FunctionalInterface 769 public interface BatchUpdateOperation { 770 /** 771 * Represents series of update operations to be applied on the datastore using the provided 772 * {@link BatchUpdater}. 773 */ apply(BatchUpdater updater)774 void apply(BatchUpdater updater); 775 } 776 777 /** 778 * Interface for staging batch update operations on a datastore. 779 * 780 * <p>Provides methods for adding different data types (boolean, int , String) to a batch update 781 * operation. 782 */ 783 public interface BatchUpdater { 784 /** 785 * Adds a boolean value to be updated in the batch operation. 786 * 787 * @throws IllegalArgumentException if {@code key} is an empty string 788 */ putBoolean(String key, boolean value)789 void putBoolean(String key, boolean value); 790 791 /** 792 * Adds an integer value to be updated in the batch operation. 793 * 794 * @throws IllegalArgumentException if {@code key} is an empty string 795 */ putInt(String key, int value)796 void putInt(String key, int value); 797 798 /** 799 * Adds a String value to be updated in the batch operation. 800 * 801 * @throws IllegalArgumentException if {@code key} is an empty string 802 */ putString(String key, String value)803 void putString(String key, String value); 804 805 /** 806 * Adds a boolean value only if the key does not already exist to be updated in the batch 807 * operation. 808 * 809 * @throws IllegalArgumentException if {@code key} is an empty string 810 */ putBooleanIfNew(String key, boolean value)811 void putBooleanIfNew(String key, boolean value); 812 813 /** 814 * Adds an integer value only if the key does not already exist to be updated in the batch 815 * operation. 816 * 817 * @throws IllegalArgumentException if {@code key} is an empty string 818 */ putIntIfNew(String key, int value)819 void putIntIfNew(String key, int value); 820 821 /** 822 * Adds a String value only if the key does not already exist to be updated in the batch 823 * operation. 824 * 825 * @throws IllegalArgumentException if {@code key} is an empty string 826 */ putStringIfNew(String key, String value)827 void putStringIfNew(String key, String value); 828 } 829 830 private static final class BatchUpdaterImpl implements BatchUpdater { 831 private final Map<String, Object> mUpdatedCachedData; 832 private boolean mChanged; 833 BatchUpdaterImpl(Map<String, Object> localMap)834 BatchUpdaterImpl(Map<String, Object> localMap) { 835 mUpdatedCachedData = new HashMap<>(localMap); 836 } 837 838 @Override putBoolean(String key, boolean value)839 public void putBoolean(String key, boolean value) { 840 putInternal(key, value); 841 } 842 843 @Override putInt(String key, int value)844 public void putInt(String key, int value) { 845 putInternal(key, value); 846 } 847 848 @Override putString(String key, String value)849 public void putString(String key, String value) { 850 putInternal(key, value); 851 } 852 853 @Override putBooleanIfNew(String key, boolean value)854 public void putBooleanIfNew(String key, boolean value) { 855 putIfNewInternal(key, value, Boolean.class); 856 } 857 858 @Override putIntIfNew(String key, int value)859 public void putIntIfNew(String key, int value) { 860 putIfNewInternal(key, value, Integer.class); 861 } 862 863 @Override putStringIfNew(String key, String value)864 public void putStringIfNew(String key, String value) { 865 putIfNewInternal(key, value, String.class); 866 } 867 isChanged()868 boolean isChanged() { 869 return mChanged; 870 } 871 putInternal(String key, Object value)872 private void putInternal(String key, Object value) { 873 checkValidKey(key); 874 Object oldValue = mUpdatedCachedData.get(key); 875 if (!value.equals(oldValue)) { 876 mUpdatedCachedData.put(key, value); 877 mChanged = true; 878 } 879 } 880 putIfNewInternal(String key, T value, Class<T> valueType)881 private <T> void putIfNewInternal(String key, T value, Class<T> valueType) { 882 checkValidKey(key); 883 884 Object valueInLocalMap = mUpdatedCachedData.get(key); 885 if (valueInLocalMap != null) { 886 checkValueType(valueInLocalMap, valueType); 887 return; 888 } 889 mUpdatedCachedData.put(key, value); 890 mChanged = true; 891 } 892 } 893 } 894