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