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.ondevicepersonalization.services.data.vendor;
18 
19 import android.annotation.NonNull;
20 import android.content.ComponentName;
21 import android.content.ContentValues;
22 import android.content.Context;
23 import android.database.Cursor;
24 import android.database.SQLException;
25 import android.database.sqlite.SQLiteDatabase;
26 import android.database.sqlite.SQLiteException;
27 
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
31 import com.android.ondevicepersonalization.services.data.DbUtils;
32 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
33 
34 import java.io.File;
35 import java.io.IOException;
36 import java.nio.file.Files;
37 import java.util.HashSet;
38 import java.util.Map;
39 import java.util.Set;
40 import java.util.concurrent.ConcurrentHashMap;
41 
42 /**
43  * Dao used to manage access to local data tables
44  */
45 public class OnDevicePersonalizationLocalDataDao {
46     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
47     private static final String TAG = "OnDevicePersonalizationLocalDataDao";
48     private static final String LOCAL_DATA_TABLE_NAME_PREFIX = "localdata";
49 
50     private static final long BLOB_SIZE_LIMIT = 100000;
51 
52     private static final Map<String, OnDevicePersonalizationLocalDataDao> sLocalDataDaos =
53             new ConcurrentHashMap<>();
54     private final OnDevicePersonalizationDbHelper mDbHelper;
55     private final ComponentName mOwner;
56     private final String mCertDigest;
57     private final String mTableName;
58     private final String mFileDir;
59 
OnDevicePersonalizationLocalDataDao(OnDevicePersonalizationDbHelper dbHelper, ComponentName owner, String certDigest, String fileDir)60     private OnDevicePersonalizationLocalDataDao(OnDevicePersonalizationDbHelper dbHelper,
61             ComponentName owner, String certDigest, String fileDir) {
62         this.mDbHelper = dbHelper;
63         this.mOwner = owner;
64         this.mCertDigest = certDigest;
65         this.mTableName = getTableName(owner, certDigest);
66         this.mFileDir = fileDir;
67     }
68 
69     /**
70      * Returns an instance of the OnDevicePersonalizationLocalDataDao given a context.
71      *
72      * @param context    The context of the application
73      * @param owner      Name of service that owns the table
74      * @param certDigest Hash of the certificate used to sign the package
75      * @return Instance of OnDevicePersonalizationLocalDataDao for accessing the requested
76      * package's table
77      */
getInstance(Context context, ComponentName owner, String certDigest)78     public static OnDevicePersonalizationLocalDataDao getInstance(Context context,
79             ComponentName owner, String certDigest) {
80         // TODO: Validate the owner and certDigest
81         String tableName = getTableName(owner, certDigest);
82         String fileDir = getFileDir(tableName, context.getFilesDir());
83         OnDevicePersonalizationLocalDataDao instance = sLocalDataDaos.get(tableName);
84         if (instance == null) {
85             synchronized (sLocalDataDaos) {
86                 instance = sLocalDataDaos.get(tableName);
87                 if (instance == null) {
88                     OnDevicePersonalizationDbHelper dbHelper =
89                             OnDevicePersonalizationDbHelper.getInstance(context);
90                     instance = new OnDevicePersonalizationLocalDataDao(
91                             dbHelper, owner, certDigest, fileDir);
92                     sLocalDataDaos.put(tableName, instance);
93                 }
94             }
95         }
96         return instance;
97     }
98 
99     /**
100      * Returns an instance of the OnDevicePersonalizationLocalDataDao given a context. This is used
101      * for testing only
102      */
103     @VisibleForTesting
getInstanceForTest(Context context, ComponentName owner, String certDigest)104     public static OnDevicePersonalizationLocalDataDao getInstanceForTest(Context context,
105             ComponentName owner, String certDigest) {
106         synchronized (OnDevicePersonalizationLocalDataDao.class) {
107             String tableName = getTableName(owner, certDigest);
108             String fileDir = getFileDir(tableName, context.getFilesDir());
109             OnDevicePersonalizationLocalDataDao instance = sLocalDataDaos.get(tableName);
110             if (instance == null) {
111                 OnDevicePersonalizationDbHelper dbHelper =
112                         OnDevicePersonalizationDbHelper.getInstanceForTest(context);
113                 instance = new OnDevicePersonalizationLocalDataDao(
114                         dbHelper, owner, certDigest, fileDir);
115                 sLocalDataDaos.put(tableName, instance);
116             }
117             return instance;
118         }
119     }
120 
121     /**
122      * Creates file directory name based on table name and base directory
123      */
getFileDir(String tableName, File baseDir)124     public static String getFileDir(String tableName, File baseDir) {
125         return baseDir + "/LocalData/" + tableName;
126     }
127 
128     /**
129      * Attempts to create the LocalData table
130      *
131      * @return true if it already exists or was created, false otherwise.
132      */
createTableIfNotExists()133     protected boolean createTableIfNotExists() {
134         try {
135             SQLiteDatabase db = mDbHelper.getWritableDatabase();
136             db.execSQL(LocalDataContract.LocalDataEntry.getCreateTableIfNotExistsStatement(
137                     mTableName));
138         } catch (SQLException e) {
139             sLogger.e(TAG + ": Failed to create table: " + mTableName, e);
140             return false;
141         }
142         // Create directory for large files
143         File dir = new File(mFileDir);
144         if (!dir.isDirectory()) {
145             return dir.mkdirs();
146         }
147         return true;
148     }
149 
150     /**
151      * Creates local data tables and adds corresponding vendor_settings metadata
152      */
createTable()153     public boolean createTable() {
154         SQLiteDatabase db = mDbHelper.getWritableDatabase();
155         try {
156             db.beginTransactionNonExclusive();
157             if (!createTableIfNotExists()) {
158                 return false;
159             }
160             if (!OnDevicePersonalizationVendorDataDao.insertNewSyncToken(db, mOwner, mCertDigest,
161                     0L)) {
162                 return false;
163             }
164             db.setTransactionSuccessful();
165         } finally {
166             db.endTransaction();
167         }
168         return true;
169     }
170 
171     /**
172      * Creates the LocalData table name for the given owner
173      */
getTableName(ComponentName owner, String certDigest)174     public static String getTableName(ComponentName owner, String certDigest) {
175         return DbUtils.getTableName(LOCAL_DATA_TABLE_NAME_PREFIX, owner, certDigest);
176     }
177 
178     /**
179      * Reads single row in the local data table
180      *
181      * @return Local data for the single row requested
182      */
readSingleLocalDataRow(String key)183     public byte[] readSingleLocalDataRow(String key) {
184         try {
185             SQLiteDatabase db = mDbHelper.getReadableDatabase();
186             String[] projection = {
187                     LocalDataContract.LocalDataEntry.TYPE,
188                     LocalDataContract.LocalDataEntry.DATA
189             };
190             String selection = LocalDataContract.LocalDataEntry.KEY + " = ?";
191             String[] selectionArgs = {key};
192             try (Cursor cursor = db.query(
193                     mTableName,
194                     projection,
195                     selection,
196                     selectionArgs,
197                     /* groupBy= */ null,
198                     /* having= */ null,
199                     /* orderBy= */ null
200             )) {
201                 if (cursor.getCount() < 1) {
202                     sLogger.d(TAG + ": Failed to find requested key: " + key);
203                     return null;
204                 }
205                 cursor.moveToNext();
206                 byte[] blob = cursor.getBlob(
207                         cursor.getColumnIndexOrThrow(LocalDataContract.LocalDataEntry.DATA));
208                 int type = cursor.getInt(
209                         cursor.getColumnIndexOrThrow(LocalDataContract.LocalDataEntry.TYPE));
210                 if (type == LocalDataContract.DATA_TYPE_FILE) {
211                     File file = new File(mFileDir, new String(blob));
212                     return Files.readAllBytes(file.toPath());
213                 }
214                 return blob;
215             }
216         } catch (SQLiteException | IOException e) {
217             sLogger.e(TAG + ": Failed to read local data row", e);
218         }
219         return null;
220     }
221 
222     /**
223      * Updates the given local data row, adds it if it doesn't already exist.
224      *
225      * @return true if the update/insert succeeded, false otherwise
226      */
updateOrInsertLocalData(LocalData localData)227     public boolean updateOrInsertLocalData(LocalData localData) {
228         long timeMillis = System.currentTimeMillis();
229         try {
230             SQLiteDatabase db = mDbHelper.getWritableDatabase();
231             ContentValues values = new ContentValues();
232             values.put(LocalDataContract.LocalDataEntry.KEY, localData.getKey());
233             if (localData.getData().length > BLOB_SIZE_LIMIT) {
234                 String filename = localData.getKey() + "_" + timeMillis;
235                 File file = new File(mFileDir, filename);
236                 Files.write(file.toPath(), localData.getData());
237                 values.put(LocalDataContract.LocalDataEntry.TYPE,
238                         LocalDataContract.DATA_TYPE_FILE);
239                 values.put(LocalDataContract.LocalDataEntry.DATA, filename.getBytes());
240             } else {
241                 values.put(LocalDataContract.LocalDataEntry.DATA, localData.getData());
242             }
243             // TODO: Cleanup file on replace instead of waiting for maintenance job.
244             return db.insertWithOnConflict(mTableName, null,
245                     values, SQLiteDatabase.CONFLICT_REPLACE) != -1;
246         } catch (SQLiteException | IOException e) {
247             sLogger.e(TAG + ": Failed to update or insert local data", e);
248             // Attempt to delete file if something failed
249             String filename = localData.getKey() + "_" + timeMillis;
250             File file = new File(mFileDir, filename);
251             file.delete();
252         }
253         return false;
254     }
255 
256     /**
257      * Deletes the row with the specified key from the local data table
258      *
259      * @param key the key specifying the row to delete
260      * @return true if the row was deleted, false otherwise.
261      */
deleteLocalDataRow(@onNull String key)262     public boolean deleteLocalDataRow(@NonNull String key) {
263         try {
264             SQLiteDatabase db = mDbHelper.getWritableDatabase();
265             String whereClause = LocalDataContract.LocalDataEntry.KEY + " = ?";
266             String[] selectionArgs = {key};
267             return db.delete(mTableName, whereClause, selectionArgs) == 1;
268         } catch (SQLiteException e) {
269             sLogger.e(TAG + ": Failed to delete row from local data", e);
270         }
271         return false;
272     }
273 
274     /**
275      * Reads all keys in the local data table
276      *
277      * @return Set of keys in the local data table.
278      */
readAllLocalDataKeys()279     public Set<String> readAllLocalDataKeys() {
280         Set<String> keyset = new HashSet<>();
281         try {
282             SQLiteDatabase db = mDbHelper.getReadableDatabase();
283             String[] projection = {VendorDataContract.VendorDataEntry.KEY};
284             try (Cursor cursor = db.query(
285                     mTableName,
286                     projection,
287                     /* selection= */ null,
288                     /* selectionArgs= */ null,
289                     /* groupBy= */ null,
290                     /* having= */ null,
291                     /* orderBy= */ null
292             )) {
293                 while (cursor.moveToNext()) {
294                     String key = cursor.getString(
295                             cursor.getColumnIndexOrThrow(VendorDataContract.VendorDataEntry.KEY));
296                     keyset.add(key);
297                 }
298                 cursor.close();
299                 return keyset;
300             }
301         } catch (SQLiteException e) {
302             sLogger.e(TAG + ": Failed to read all vendor data keys", e);
303         }
304         return keyset;
305     }
306 
307     /**
308      * Deletes LocalData table for given owner
309      */
deleteTable(Context context, ComponentName owner, String certDigest)310     public static void deleteTable(Context context, ComponentName owner, String certDigest) {
311         OnDevicePersonalizationDbHelper dbHelper =
312                 OnDevicePersonalizationDbHelper.getInstance(context);
313         SQLiteDatabase db = dbHelper.getWritableDatabase();
314         db.execSQL("DROP TABLE IF EXISTS " + getTableName(owner, certDigest));
315     }
316 }
317