• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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