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.adservices.data.signals; 18 19 import android.adservices.common.AdTechIdentifier; 20 import android.annotation.SuppressLint; 21 import android.content.Context; 22 import android.util.AtomicFile; 23 24 import androidx.annotation.NonNull; 25 26 import com.android.adservices.LoggerFactory; 27 import com.android.adservices.service.common.compat.FileCompatUtils; 28 import com.android.internal.annotations.VisibleForTesting; 29 30 import java.io.File; 31 import java.io.FileNotFoundException; 32 import java.io.FileOutputStream; 33 import java.io.IOException; 34 import java.nio.charset.StandardCharsets; 35 import java.util.Objects; 36 37 /** 38 * Handles persistence and retrieval of encoding logic for buyers. By leveraging Atomic files it 39 * ensures that we do not read half written encoders. This persistence layer is not strictly 40 * sequential, and will honor the last completed write for parallel writes. Multiple encoder write 41 * updates are unlikely to happen. 42 */ 43 public class EncoderPersistenceDao { 44 45 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 46 47 /** 48 * TODO(ag/24355874) : remove this once ag/24355874 gets merged and utility for filename prefix 49 * becomes available 50 */ 51 @VisibleForTesting static final String ADSERVICES_PREFIX = "adservices_"; 52 53 @VisibleForTesting static final String ENCODERS_DIR = ADSERVICES_PREFIX + "encoders"; 54 @VisibleForTesting static final String ENCODER_FILE_SUFFIX = ".encoder"; 55 56 @NonNull private File mFilesDir; 57 private static final Object SINGLETON_LOCK = new Object(); 58 private static volatile EncoderPersistenceDao sInstance; 59 60 @SuppressLint("NewAdServicesFile") EncoderPersistenceDao(Context context)61 private EncoderPersistenceDao(Context context) { 62 this.mFilesDir = context.getFilesDir(); 63 } 64 65 /** Provides a singleton instance of {@link EncoderPersistenceDao} */ getInstance(@onNull Context context)66 public static EncoderPersistenceDao getInstance(@NonNull Context context) { 67 Objects.requireNonNull(context, "Context must not be null"); 68 69 EncoderPersistenceDao singleInstance = sInstance; 70 if (singleInstance != null) { 71 return singleInstance; 72 } 73 74 synchronized (SINGLETON_LOCK) { 75 if (sInstance == null) { 76 sInstance = new EncoderPersistenceDao(context); 77 } 78 } 79 return sInstance; 80 } 81 82 /** 83 * Stores encoding logic for a buyer 84 * 85 * @param buyer Ad tech for which encoding logic needs to be persisted 86 * @param encodingLogic for encoding raw signals 87 * @return true, if successfully created and written 88 */ persistEncoder(@onNull AdTechIdentifier buyer, @NonNull String encodingLogic)89 public boolean persistEncoder(@NonNull AdTechIdentifier buyer, @NonNull String encodingLogic) { 90 File encoderDir = createEncodersDirectoryIfDoesNotExist(); 91 String uniqueFileNamePerBuyer = generateFileNameForBuyer(buyer); 92 File encoderFile = createFileInDirectory(encoderDir, uniqueFileNamePerBuyer); 93 return writeDataToFile(encoderFile, encodingLogic); 94 } 95 96 /** 97 * Fetches encoding logic for a buyer 98 * 99 * @param buyer Ad tech for which encoding logic is persisted 100 * @return the encoding logic as a String, if not present or in error returns an empty string 101 */ getEncoder(@onNull AdTechIdentifier buyer)102 public String getEncoder(@NonNull AdTechIdentifier buyer) { 103 File encoderDir = createEncodersDirectoryIfDoesNotExist(); 104 105 String uniqueFileNamePerBuyer = generateFileNameForBuyer(buyer); 106 return readDataFromFile(encoderDir, uniqueFileNamePerBuyer); 107 } 108 109 /** 110 * Deletes encoding logic for a buyer 111 * 112 * @param buyer Ad tech for which encoding logic needs to be deleted 113 * @return true if the encoding logic never existed or was successfully deleted 114 */ deleteEncoder(@onNull AdTechIdentifier buyer)115 public boolean deleteEncoder(@NonNull AdTechIdentifier buyer) { 116 String uniqueFileNamePerBuyer = generateFileNameForBuyer(buyer); 117 File file = 118 FileCompatUtils.newFileHelper( 119 FileCompatUtils.newFileHelper(mFilesDir, ENCODERS_DIR), 120 uniqueFileNamePerBuyer); 121 boolean deletionComplete = false; 122 if (!file.exists()) { 123 deletionComplete = true; 124 } else { 125 AtomicFile atomicFile = new AtomicFile(file); 126 atomicFile.delete(); 127 deletionComplete = !file.exists(); 128 } 129 return deletionComplete; 130 } 131 132 /** 133 * Deletes all encoders persisted ever persisted 134 * 135 * @return true if the encoding logics were all deleted 136 */ deleteAllEncoders()137 public boolean deleteAllEncoders() { 138 return deleteDirectory(createEncodersDirectoryIfDoesNotExist()); 139 } 140 141 @VisibleForTesting createEncodersDirectoryIfDoesNotExist()142 File createEncodersDirectoryIfDoesNotExist() { 143 // This itself does not create a directory or file 144 File encodersDir = FileCompatUtils.newFileHelper(mFilesDir, ENCODERS_DIR); 145 if (!encodersDir.exists()) { 146 147 // This creates the actual directory 148 if (encodersDir.mkdirs()) { 149 sLogger.v("New Encoders directory creation succeeded"); 150 } else { 151 sLogger.e("New Encoders directory creation failed"); 152 } 153 } else { 154 sLogger.v("Encoders directory already exists at :" + encodersDir.getPath()); 155 } 156 return encodersDir; 157 } 158 159 @VisibleForTesting createFileInDirectory(File directory, String fileName)160 File createFileInDirectory(File directory, String fileName) { 161 // This itself does not create a directory or file 162 File file = FileCompatUtils.newFileHelper(directory, fileName); 163 if (!file.isFile()) { 164 try { 165 // This creates the actual file 166 if (file.createNewFile()) { 167 sLogger.v("New Encoder file creation succeeded"); 168 } else { 169 sLogger.e("New Encoder file creation failed"); 170 } 171 } catch (IOException e) { 172 sLogger.e("Exception trying to create the file"); 173 } 174 } else { 175 sLogger.v("Encoder file already exists at :" + file.getPath()); 176 } 177 return file; 178 } 179 180 @VisibleForTesting writeDataToFile(File file, String data)181 boolean writeDataToFile(File file, String data) { 182 FileOutputStream fos = null; 183 AtomicFile atomicFile = new AtomicFile(file); 184 try { 185 fos = atomicFile.startWrite(); 186 fos.write(data.getBytes(StandardCharsets.UTF_8)); 187 atomicFile.finishWrite(fos); 188 // If successful return true 189 return true; 190 } catch (FileNotFoundException e) { 191 sLogger.e(String.format("Could not find file: %s", file.getName())); 192 failWriteToFile(fos, atomicFile); 193 } catch (IOException e) { 194 sLogger.e(String.format("Could not write to file: %s", file.getName())); 195 failWriteToFile(fos, atomicFile); 196 } 197 return false; 198 } 199 200 /** Closes the file output stream associated with the atomic file */ failWriteToFile(FileOutputStream fos, AtomicFile atomicFile)201 private void failWriteToFile(FileOutputStream fos, AtomicFile atomicFile) { 202 if (fos != null && atomicFile != null) { 203 atomicFile.failWrite(fos); 204 } 205 } 206 207 @VisibleForTesting readDataFromFile(File directory, String fileName)208 String readDataFromFile(File directory, String fileName) { 209 try { 210 // This does not create a new file 211 File file = FileCompatUtils.newFileHelper(directory, fileName); 212 AtomicFile atomicFile = new AtomicFile(file); 213 byte[] fileContents = atomicFile.readFully(); 214 return new String(fileContents, StandardCharsets.UTF_8); 215 } catch (IOException e) { 216 sLogger.e(String.format("Exception trying to read file: %s", fileName)); 217 } 218 return null; 219 } 220 221 @VisibleForTesting deleteDirectory(File directory)222 boolean deleteDirectory(File directory) { 223 if (directory.exists() && directory.isDirectory()) { 224 File[] children = directory.listFiles(); 225 if (children != null) { 226 for (File child : children) { 227 sLogger.v( 228 String.format( 229 "Deleting from path: %s , file: %s", 230 child.getPath(), child.getName())); 231 AtomicFile atomicFile = new AtomicFile(child); 232 atomicFile.delete(); 233 } 234 } 235 } 236 // This only succeeds if the children files have been deleted first 237 return directory.delete(); 238 } 239 240 /** 241 * Explicitly avoids filename format being changed across systems for a buyer, by giving control 242 * to the persistence layer on how to decide a filename. 243 * 244 * @param buyer Ad tech for which the file has to be stored 245 * @return the String representing filename for the buyer 246 */ 247 @VisibleForTesting generateFileNameForBuyer(@onNull AdTechIdentifier buyer)248 String generateFileNameForBuyer(@NonNull AdTechIdentifier buyer) { 249 Objects.requireNonNull(buyer); 250 return ADSERVICES_PREFIX + buyer + ENCODER_FILE_SUFFIX; 251 } 252 } 253