1 /*
2  * Copyright (C) 2023 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.server.ondevicepersonalization;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.os.PersistableBundle;
22 import android.util.AtomicFile;
23 
24 import com.android.internal.annotations.GuardedBy;
25 import com.android.internal.annotations.VisibleForTesting;
26 import com.android.internal.util.Preconditions;
27 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
28 
29 import java.io.ByteArrayInputStream;
30 import java.io.ByteArrayOutputStream;
31 import java.io.File;
32 import java.io.FileNotFoundException;
33 import java.io.FileOutputStream;
34 import java.io.IOException;
35 import java.util.HashMap;
36 import java.util.Map;
37 import java.util.Objects;
38 import java.util.Set;
39 import java.util.concurrent.locks.Lock;
40 import java.util.concurrent.locks.ReadWriteLock;
41 import java.util.concurrent.locks.ReentrantReadWriteLock;
42 
43 /**
44  * A generic key-value datastore utilizing {@link android.util.AtomicFile} and {@link
45  * android.os.PersistableBundle} to read/write a simple key/value map to file.
46  * This class is thread-safe.
47  * @hide
48  */
49 public class BooleanFileDataStore {
50     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
51     private static final String TAG = "BooleanFileDataStore";
52     private final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock();
53     private final Lock mReadLock = mReadWriteLock.readLock();
54     private final Lock mWriteLock = mReadWriteLock.writeLock();
55 
56     private final AtomicFile mAtomicFile;
57     private final Map<String, Boolean> mLocalMap = new HashMap<>();
58 
59     // TODO (b/300993651): make the datastore access singleton.
60     // TODO (b/301131410): add version history feature.
BooleanFileDataStore(@onNull String parentPath, @NonNull String filename)61     public BooleanFileDataStore(@NonNull String parentPath, @NonNull String filename) {
62         Objects.requireNonNull(parentPath);
63         Objects.requireNonNull(filename);
64         Preconditions.checkStringNotEmpty(parentPath);
65         Preconditions.checkStringNotEmpty(filename);
66         mAtomicFile = new AtomicFile(new File(parentPath, filename));
67     }
68 
69     /**
70      * Loads data from the datastore file on disk.
71      * @throws IOException if file read fails.
72      */
initialize()73     public void initialize() throws IOException {
74         sLogger.d(TAG + ": reading from file " + mAtomicFile.getBaseFile());
75         mReadLock.lock();
76         try {
77             readFromFile();
78         } finally {
79             mReadLock.unlock();
80         }
81     }
82 
83     /**
84      * Stores a value to the datastore file, which is immediately committed.
85      * @param key A non-null, non-empty String to store the {@code value}.
86      * @param value A boolean to be stored.
87      * @throws IOException if file write fails.
88      * @throws NullPointerException if {@code key} is null.
89      * @throws IllegalArgumentException if (@code key) is an empty string.
90      */
put(@onNull String key, boolean value)91     public void put(@NonNull String key, boolean value) throws IOException {
92         Objects.requireNonNull(key);
93         Preconditions.checkStringNotEmpty(key, "Key must not be empty.");
94 
95         mWriteLock.lock();
96         try {
97             mLocalMap.put(key, value);
98             writeToFile();
99         } finally {
100             mWriteLock.unlock();
101         }
102     }
103 
104     /**
105      * Retrieves a boolean value from the loaded datastore file.
106      *
107      * @param key A non-null, non-empty String key to fetch a value from.
108      * @return The boolean value stored against a {@code key}, or null if it doesn't exist.
109      * @throws IllegalArgumentException if {@code key} is an empty string.
110      * @throws NullPointerException if {@code key} is null.
111      */
112     @Nullable
get(@onNull String key)113     public Boolean get(@NonNull String key) {
114         Objects.requireNonNull(key);
115         Preconditions.checkStringNotEmpty(key);
116 
117         mReadLock.lock();
118         try {
119             return mLocalMap.get(key);
120         } finally {
121             mReadLock.unlock();
122         }
123     }
124 
125     /**
126      * Retrieves a {@link Set} of all keys loaded from the datastore file.
127      *
128      * @return A {@link Set} of {@link String} keys currently in the loaded datastore
129      */
130     @NonNull
keySet()131     public Set<String> keySet() {
132         mReadLock.lock();
133         try {
134             return Set.copyOf(mLocalMap.keySet());
135         } finally {
136             mReadLock.unlock();
137         }
138     }
139 
140     /**
141      * Clears all entries from the datastore file and committed immediately.
142      *
143      * @throws IOException if file write fails.
144      */
clear()145     public void clear() throws IOException {
146         sLogger.d(TAG + ": clearing all entries from datastore");
147 
148         mWriteLock.lock();
149         try {
150             mLocalMap.clear();
151             writeToFile();
152         } finally {
153             mWriteLock.unlock();
154         }
155     }
156 
157     @GuardedBy("mWriteLock")
writeToFile()158     private void writeToFile() throws IOException {
159         final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
160         final PersistableBundle persistableBundle = new PersistableBundle();
161         for (Map.Entry<String, Boolean> entry: mLocalMap.entrySet()) {
162             persistableBundle.putBoolean(entry.getKey(), entry.getValue());
163         }
164 
165         persistableBundle.writeToStream(outputStream);
166 
167         FileOutputStream out = null;
168         try {
169             out = mAtomicFile.startWrite();
170             out.write(outputStream.toByteArray());
171             mAtomicFile.finishWrite(out);
172         } catch (IOException e) {
173             mAtomicFile.failWrite(out);
174             sLogger.e(TAG + ": write to file " + mAtomicFile.getBaseFile() + " failed.");
175             throw e;
176         }
177     }
178 
179     @GuardedBy("mReadLock")
readFromFile()180     private void readFromFile() throws IOException {
181         try {
182             final ByteArrayInputStream inputStream = new ByteArrayInputStream(
183                             mAtomicFile.readFully());
184             final PersistableBundle persistableBundle = PersistableBundle.readFromStream(
185                             inputStream);
186 
187             mLocalMap.clear();
188             for (String key: persistableBundle.keySet()) {
189                 mLocalMap.put(key, persistableBundle.getBoolean(key));
190             }
191         } catch (FileNotFoundException e) {
192             sLogger.d(TAG + ": file not found exception.");
193             mLocalMap.clear();
194         } catch (IOException e) {
195             sLogger.e(TAG + ": read from " + mAtomicFile.getBaseFile() + " failed");
196             throw e;
197         }
198     }
199 
200     /**
201      * Delete the datastore file for testing.
202      */
203     @VisibleForTesting
tearDownForTesting()204     public void tearDownForTesting() {
205         mWriteLock.lock();
206         try {
207             mAtomicFile.delete();
208             mLocalMap.clear();
209         } finally {
210             mWriteLock.unlock();
211         }
212     }
213 
214     /**
215      * Clear the loaded content from local map for testing.
216      */
217     @VisibleForTesting
clearLocalMapForTesting()218     public void clearLocalMapForTesting() {
219         mWriteLock.lock();
220         mLocalMap.clear();
221         mWriteLock.unlock();
222     }
223 }
224