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