• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.ContentValues;
20 import android.content.Context;
21 import android.database.Cursor;
22 import android.database.SQLException;
23 import android.database.sqlite.SQLiteDatabase;
24 import android.database.sqlite.SQLiteException;
25 import android.util.Log;
26 
27 import com.android.internal.annotations.VisibleForTesting;
28 import com.android.ondevicepersonalization.services.data.OnDevicePersonalizationDbHelper;
29 
30 import java.util.AbstractMap;
31 import java.util.ArrayList;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Set;
37 import java.util.stream.Collectors;
38 
39 /**
40  * Dao used to manage access to vendor data tables
41  */
42 public class OnDevicePersonalizationVendorDataDao {
43     private static final String TAG = "OnDevicePersonalizationVendorDataDao";
44     private static final String VENDOR_DATA_TABLE_NAME_PREFIX = "vendordata_";
45 
46     private static final Map<String, OnDevicePersonalizationVendorDataDao> sVendorDataDaos =
47             new HashMap<>();
48     private final OnDevicePersonalizationDbHelper mDbHelper;
49     private final String mOwner;
50     private final String mCertDigest;
51     private final String mTableName;
52 
OnDevicePersonalizationVendorDataDao(OnDevicePersonalizationDbHelper dbHelper, String owner, String certDigest)53     private OnDevicePersonalizationVendorDataDao(OnDevicePersonalizationDbHelper dbHelper,
54             String owner, String certDigest) {
55         this.mDbHelper = dbHelper;
56         this.mOwner = owner;
57         this.mCertDigest = certDigest;
58         this.mTableName = getTableName(owner, certDigest);
59     }
60 
61     /**
62      * Returns an instance of the OnDevicePersonalizationVendorDataDao given a context.
63      *
64      * @param context    The context of the application
65      * @param owner      Name of package that owns the table
66      * @param certDigest Hash of the certificate used to sign the package
67      * @return Instance of OnDevicePersonalizationVendorDataDao for accessing the requested
68      * package's table
69      */
getInstance(Context context, String owner, String certDigest)70     public static OnDevicePersonalizationVendorDataDao getInstance(Context context, String owner,
71             String certDigest) {
72         synchronized (OnDevicePersonalizationVendorDataDao.class) {
73             // TODO: Validate the owner and certDigest
74             String tableName = getTableName(owner, certDigest);
75             OnDevicePersonalizationVendorDataDao instance = sVendorDataDaos.get(tableName);
76             if (instance == null) {
77                 OnDevicePersonalizationDbHelper dbHelper =
78                         OnDevicePersonalizationDbHelper.getInstance(context);
79                 instance = new OnDevicePersonalizationVendorDataDao(
80                         dbHelper, owner, certDigest);
81                 sVendorDataDaos.put(tableName, instance);
82             }
83             return instance;
84         }
85     }
86 
87     /**
88      * Returns an instance of the OnDevicePersonalizationVendorDataDao given a context. This is used
89      * for testing only
90      */
91     @VisibleForTesting
getInstanceForTest(Context context, String owner, String certDigest)92     public static OnDevicePersonalizationVendorDataDao getInstanceForTest(Context context,
93             String owner, String certDigest) {
94         synchronized (OnDevicePersonalizationVendorDataDao.class) {
95             String tableName = getTableName(owner, certDigest);
96             OnDevicePersonalizationVendorDataDao instance = sVendorDataDaos.get(tableName);
97             if (instance == null) {
98                 OnDevicePersonalizationDbHelper dbHelper =
99                         OnDevicePersonalizationDbHelper.getInstanceForTest(context);
100                 instance = new OnDevicePersonalizationVendorDataDao(
101                         dbHelper, owner, certDigest);
102                 sVendorDataDaos.put(tableName, instance);
103             }
104             return instance;
105         }
106     }
107 
getTableName(String owner, String certDigest)108     private static String getTableName(String owner, String certDigest) {
109         owner = owner.replace(".", "_");
110         return VENDOR_DATA_TABLE_NAME_PREFIX + owner + "_" + certDigest;
111     }
112 
113     /**
114      * Gets the name and cert of all vendors with VendorData & VendorSettings
115      */
getVendors(Context context)116     public static List<Map.Entry<String, String>> getVendors(Context context) {
117         OnDevicePersonalizationDbHelper dbHelper =
118                 OnDevicePersonalizationDbHelper.getInstance(context);
119         SQLiteDatabase db = dbHelper.getReadableDatabase();
120         String[] projection = {VendorSettingsContract.VendorSettingsEntry.OWNER,
121                 VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST};
122         Cursor cursor = db.query(
123                 /* distinct= */ true,
124                 VendorSettingsContract.VendorSettingsEntry.TABLE_NAME,
125                 projection,
126                 /* selection= */ null,
127                 /* selectionArgs= */ null,
128                 /* groupBy= */ null,
129                 /* having= */ null,
130                 /* orderBy= */ null,
131                 /* limit= */ null
132         );
133 
134         List<Map.Entry<String, String>> result = new ArrayList<>();
135         try {
136             while (cursor.moveToNext()) {
137                 String owner = cursor.getString(cursor.getColumnIndexOrThrow(
138                         VendorSettingsContract.VendorSettingsEntry.OWNER));
139                 String cert = cursor.getString(cursor.getColumnIndexOrThrow(
140                         VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST));
141                 result.add(new AbstractMap.SimpleImmutableEntry<>(owner, cert));
142             }
143         } catch (Exception e) {
144             Log.e(TAG, "Failed to get Vendors", e);
145         } finally {
146             cursor.close();
147         }
148         return result;
149     }
150 
151     /**
152      * Performs a transaction to delete the vendorData table and vendorSettings for a given package.
153      */
deleteVendorData(Context context, String owner, String certDigest)154     public static boolean deleteVendorData(Context context, String owner, String certDigest) {
155         OnDevicePersonalizationDbHelper dbHelper =
156                 OnDevicePersonalizationDbHelper.getInstance(context);
157         SQLiteDatabase db = dbHelper.getWritableDatabase();
158         String vendorDataTableName = getTableName(owner, certDigest);
159         try {
160             db.beginTransactionNonExclusive();
161             // Delete rows from VendorSettings
162             String selection = VendorSettingsContract.VendorSettingsEntry.OWNER + " = ? AND "
163                     + VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST + " = ?";
164             String[] selectionArgs = {owner, certDigest};
165             db.delete(VendorSettingsContract.VendorSettingsEntry.TABLE_NAME, selection,
166                     selectionArgs);
167 
168             // Delete the vendorData table
169             db.execSQL("DROP TABLE " + vendorDataTableName);
170             OnDevicePersonalizationLocalDataDao.deleteTable(context, owner, certDigest);
171 
172             db.setTransactionSuccessful();
173         } catch (Exception e) {
174             Log.e(TAG, "Failed to delete vendorData for: " + owner, e);
175             return false;
176         } finally {
177             db.endTransaction();
178         }
179         return true;
180     }
181 
createTableIfNotExists(String tableName)182     private boolean createTableIfNotExists(String tableName) {
183         try {
184             SQLiteDatabase db = mDbHelper.getWritableDatabase();
185             db.execSQL(VendorDataContract.VendorDataEntry.getCreateTableIfNotExistsStatement(
186                     tableName));
187         } catch (SQLException e) {
188             Log.e(TAG, "Failed to create table: " + tableName, e);
189             return false;
190         }
191         return true;
192     }
193 
194     /**
195      * Reads all rows in the vendor data table
196      *
197      * @return Cursor of all rows in table
198      */
readAllVendorData()199     public Cursor readAllVendorData() {
200         try {
201             SQLiteDatabase db = mDbHelper.getReadableDatabase();
202             return db.query(
203                     mTableName,
204                     /* columns= */ null,
205                     /* selection= */ null,
206                     /* selectionArgs= */ null,
207                     /* groupBy= */ null,
208                     /* having= */ null,
209                     /* orderBy= */ null
210             );
211         } catch (SQLiteException e) {
212             Log.e(TAG, "Failed to read vendor data rows", e);
213         }
214         return null;
215     }
216 
217     /**
218      * Reads single row in the vendor data table
219      *
220      * @return Vendor data for the single row requested
221      */
readSingleVendorDataRow(String key)222     public byte[] readSingleVendorDataRow(String key) {
223         try {
224             SQLiteDatabase db = mDbHelper.getReadableDatabase();
225             String[] projection = {VendorDataContract.VendorDataEntry.DATA};
226             String selection = VendorDataContract.VendorDataEntry.KEY + " = ?";
227             String[] selectionArgs = {key};
228             try (Cursor cursor = db.query(
229                     mTableName,
230                     projection,
231                     selection,
232                     selectionArgs,
233                     /* groupBy= */ null,
234                     /* having= */ null,
235                     /* orderBy= */ null
236             )) {
237                 if (cursor.getCount() < 1) {
238                     Log.d(TAG, "Failed to find requested key: " + key);
239                     return null;
240                 }
241                 cursor.moveToNext();
242                 return cursor.getBlob(0);
243             }
244         } catch (SQLiteException e) {
245             Log.e(TAG, "Failed to read vendor data row", e);
246         }
247         return null;
248     }
249 
250     /**
251      * Reads all keys in the vendor data table
252      *
253      * @return Set of keys in the vendor data table.
254      */
readAllVendorDataKeys()255     public Set<String> readAllVendorDataKeys() {
256         Set<String> keyset = new HashSet<>();
257         try {
258             SQLiteDatabase db = mDbHelper.getReadableDatabase();
259             String[] projection = {VendorDataContract.VendorDataEntry.KEY};
260             try (Cursor cursor = db.query(
261                     mTableName,
262                     projection,
263                     /* selection= */ null,
264                     /* selectionArgs= */ null,
265                     /* groupBy= */ null,
266                     /* having= */ null,
267                     /* orderBy= */ null
268             )) {
269                 while (cursor.moveToNext()) {
270                     String key = cursor.getString(
271                             cursor.getColumnIndexOrThrow(VendorDataContract.VendorDataEntry.KEY));
272                     keyset.add(key);
273                 }
274                 cursor.close();
275                 return keyset;
276             }
277         } catch (SQLiteException e) {
278             Log.e(TAG, "Failed to read all vendor data keys", e);
279         }
280         return keyset;
281     }
282 
283     /**
284      * Batch updates and/or inserts a list of vendor data and a corresponding syncToken and
285      * deletes unretained keys.
286      *
287      * @return true if the transaction is successful. False otherwise.
288      */
batchUpdateOrInsertVendorDataTransaction(List<VendorData> vendorDataList, List<String> retainedKeys, long syncToken)289     public boolean batchUpdateOrInsertVendorDataTransaction(List<VendorData> vendorDataList,
290             List<String> retainedKeys, long syncToken) {
291         SQLiteDatabase db = mDbHelper.getWritableDatabase();
292         try {
293             db.beginTransactionNonExclusive();
294             if (!createTableIfNotExists(mTableName)) {
295                 return false;
296             }
297             if (!OnDevicePersonalizationLocalDataDao.createTableIfNotExists(
298                     OnDevicePersonalizationLocalDataDao.getTableName(mOwner, mCertDigest),
299                     mDbHelper)) {
300                 return false;
301             }
302             if (!deleteUnretainedRows(retainedKeys)) {
303                 return false;
304             }
305             for (VendorData vendorData : vendorDataList) {
306                 if (!updateOrInsertVendorData(vendorData)) {
307                     // The query failed. Return and don't finalize the transaction.
308                     return false;
309                 }
310             }
311             if (!updateOrInsertSyncToken(syncToken)) {
312                 return false;
313             }
314             db.setTransactionSuccessful();
315         } finally {
316             db.endTransaction();
317         }
318         return true;
319     }
320 
deleteUnretainedRows(List<String> retainedKeys)321     private boolean deleteUnretainedRows(List<String> retainedKeys) {
322         try {
323             SQLiteDatabase db = mDbHelper.getWritableDatabase();
324             String retainedKeysString = retainedKeys.stream().map(s -> "'" + s + "'").collect(
325                     Collectors.joining(",", "(", ")"));
326             String whereClause = VendorDataContract.VendorDataEntry.KEY + " NOT IN "
327                     + retainedKeysString;
328             return db.delete(mTableName, whereClause,
329                     null) != -1;
330         } catch (SQLiteException e) {
331             Log.e(TAG, "Failed to delete unretained rows", e);
332         }
333         return false;
334     }
335 
336     /**
337      * Updates the given vendor data row, adds it if it doesn't already exist.
338      *
339      * @return true if the update/insert succeeded, false otherwise
340      */
updateOrInsertVendorData(VendorData vendorData)341     private boolean updateOrInsertVendorData(VendorData vendorData) {
342         try {
343             SQLiteDatabase db = mDbHelper.getWritableDatabase();
344             ContentValues values = new ContentValues();
345             values.put(VendorDataContract.VendorDataEntry.KEY, vendorData.getKey());
346             values.put(VendorDataContract.VendorDataEntry.DATA, vendorData.getData());
347             return db.insertWithOnConflict(mTableName, null,
348                     values, SQLiteDatabase.CONFLICT_REPLACE) != -1;
349         } catch (SQLiteException e) {
350             Log.e(TAG, "Failed to update or insert buyer data", e);
351         }
352         return false;
353     }
354 
355     /**
356      * Updates the syncToken, adds it if it doesn't already exist.
357      *
358      * @return true if the update/insert succeeded, false otherwise
359      */
updateOrInsertSyncToken(long syncToken)360     private boolean updateOrInsertSyncToken(long syncToken) {
361         try {
362             SQLiteDatabase db = mDbHelper.getWritableDatabase();
363             ContentValues values = new ContentValues();
364             values.put(VendorSettingsContract.VendorSettingsEntry.OWNER, mOwner);
365             values.put(VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST, mCertDigest);
366             values.put(VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN, syncToken);
367             return db.insertWithOnConflict(VendorSettingsContract.VendorSettingsEntry.TABLE_NAME,
368                     null, values, SQLiteDatabase.CONFLICT_REPLACE) != -1;
369         } catch (SQLiteException e) {
370             Log.e(TAG, "Failed to update or insert syncToken", e);
371         }
372         return false;
373     }
374 
375     /**
376      * Gets the syncToken owned by {@link #mOwner} with cert {@link #mCertDigest}
377      *
378      * @return syncToken if found, -1 otherwise
379      */
getSyncToken()380     public long getSyncToken() {
381         SQLiteDatabase db = mDbHelper.getReadableDatabase();
382         String selection = VendorSettingsContract.VendorSettingsEntry.OWNER + " = ? AND "
383                 + VendorSettingsContract.VendorSettingsEntry.CERT_DIGEST + " = ?";
384         String[] selectionArgs = {mOwner, mCertDigest};
385         String[] projection = {VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN};
386         Cursor cursor = db.query(
387                 VendorSettingsContract.VendorSettingsEntry.TABLE_NAME,
388                 projection,
389                 selection,
390                 selectionArgs,
391                 /* groupBy= */ null,
392                 /* having= */ null,
393                 /* orderBy= */ null
394         );
395         try {
396             if (cursor.moveToFirst()) {
397                 return cursor.getLong(cursor.getColumnIndexOrThrow(
398                         VendorSettingsContract.VendorSettingsEntry.SYNC_TOKEN));
399             }
400         } catch (SQLiteException e) {
401             Log.e(TAG, "Failed to update or insert syncToken", e);
402         } finally {
403             cursor.close();
404         }
405         return -1;
406     }
407 }
408