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