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