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.shared; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATABASE_READ_EXCEPTION; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATABASE_WRITE_EXCEPTION; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON; 22 23 import android.annotation.Nullable; 24 import android.content.ContentValues; 25 import android.content.Context; 26 import android.database.Cursor; 27 import android.database.DatabaseUtils; 28 import android.database.sqlite.SQLiteDatabase; 29 import android.database.sqlite.SQLiteException; 30 import android.database.sqlite.SQLiteOpenHelper; 31 import android.net.Uri; 32 33 import com.android.adservices.LoggerFactory; 34 import com.android.adservices.data.DbHelper; 35 import com.android.adservices.data.encryptionkey.EncryptionKeyTables; 36 import com.android.adservices.data.enrollment.EnrollmentTables; 37 import com.android.adservices.data.enrollment.SqliteObjectMapper; 38 import com.android.adservices.data.shared.migration.ISharedDbMigrator; 39 import com.android.adservices.data.shared.migration.SharedDbMigratorV2; 40 import com.android.adservices.data.shared.migration.SharedDbMigratorV3; 41 import com.android.adservices.data.shared.migration.SharedDbMigratorV4; 42 import com.android.adservices.errorlogging.ErrorLogUtil; 43 import com.android.adservices.service.FlagsFactory; 44 import com.android.adservices.service.common.WebAddresses; 45 import com.android.adservices.service.common.compat.FileCompatUtils; 46 import com.android.adservices.service.enrollment.EnrollmentData; 47 import com.android.adservices.shared.common.ApplicationContextSingleton; 48 import com.android.internal.annotations.VisibleForTesting; 49 50 import com.google.common.collect.ImmutableList; 51 52 import java.io.File; 53 import java.util.ArrayList; 54 import java.util.Arrays; 55 import java.util.HashMap; 56 import java.util.HashSet; 57 import java.util.List; 58 import java.util.Map; 59 import java.util.Optional; 60 import java.util.Set; 61 import java.util.stream.Collectors; 62 import java.util.stream.Stream; 63 64 /** Database Helper for Shared AdServices database. */ 65 public class SharedDbHelper extends SQLiteOpenHelper { 66 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 67 68 private static final String DATABASE_NAME = 69 FileCompatUtils.getAdservicesFilename("adservices_shared.db"); 70 public static final int DATABASE_VERSION_V3 = 3; 71 // Version 4: Adds enrolled_apis and enrolled_site columns to enrollment table, guarded by 72 // feature flag 73 public static final int DATABASE_VERSION_V4 = 4; 74 private static SharedDbHelper sSingleton; 75 private final File mDbFile; 76 private final int mDbVersion; 77 private final DbHelper mDbHelper; 78 79 @VisibleForTesting SharedDbHelper(Context context, String dbName, int dbVersion, DbHelper dbHelper)80 public SharedDbHelper(Context context, String dbName, int dbVersion, DbHelper dbHelper) { 81 super(context, dbName, null, dbVersion); 82 mDbFile = FileCompatUtils.getDatabasePathHelper(context, dbName); 83 this.mDbVersion = dbVersion; 84 this.mDbHelper = dbHelper; 85 } 86 87 /** Returns an instance of the SharedDbHelper given a context. */ getInstance()88 public static SharedDbHelper getInstance() { 89 synchronized (SharedDbHelper.class) { 90 if (sSingleton == null) { 91 sSingleton = 92 new SharedDbHelper( 93 ApplicationContextSingleton.get(), 94 DATABASE_NAME, 95 getDatabaseVersionToCreate(), 96 DbHelper.getInstance()); 97 } 98 return sSingleton; 99 } 100 } 101 102 @Override onCreate(SQLiteDatabase db)103 public void onCreate(SQLiteDatabase db) { 104 sLogger.d( 105 "SharedDbHelper.onCreate with version %d. Name: %s", mDbVersion, mDbFile.getName()); 106 SQLiteDatabase oldDb = mDbHelper.safeGetWritableDatabase(); 107 // Only need to check enrollment table, as that one was the only table living previously in 108 // adservices.db 109 if (hasAllTables(oldDb, EnrollmentTables.ENROLLMENT_TABLES)) { 110 // Create encryption key table V2 schema so migration works as expected 111 createEncryptionKeyV2Schema(db); 112 // Copy enrollment data from old db to new db. 113 migrateEnrollmentTables(db, oldDb); 114 // Update new db from version 1 to latest 115 upgradeSchema(db, 1, mDbVersion); 116 } else { 117 // Create current schema for both enrollment and encryption key tables. 118 createSchema(db); 119 } 120 } 121 migrateEnrollmentTables(SQLiteDatabase db, SQLiteDatabase oldDb)122 private void migrateEnrollmentTables(SQLiteDatabase db, SQLiteDatabase oldDb) { 123 sLogger.d("SharedDbHelper.migrateEnrollmentTables copying Enrollment data from old db"); 124 // Migrate Data: 125 // 1. Create V1 (old DbHelper's last database version) version of tables 126 createEnrollmentV1Schema(db); 127 // 2. Copy data from old database 128 migrateOldDataToNewDatabase(oldDb, db, EnrollmentTables.ENROLLMENT_TABLES); 129 } 130 131 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)132 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 133 sLogger.d( 134 "SharedDbHelper.onUpgrade. Attempting to upgrade version from %d to %d.", 135 oldVersion, newVersion); 136 upgradeSchema(db, oldVersion, newVersion); 137 } 138 139 @Override onOpen(SQLiteDatabase db)140 public void onOpen(SQLiteDatabase db) { 141 super.onOpen(db); 142 } 143 144 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) getOrderedDbMigrators()145 public List<ISharedDbMigrator> getOrderedDbMigrators() { 146 return ImmutableList.of( 147 new SharedDbMigratorV2(), new SharedDbMigratorV3(), new SharedDbMigratorV4()); 148 } 149 150 /** 151 * Check whether enrolled_apis and enrolled_site column is supported in Enrollment Table. These 152 * columns are introduced in Version 4. 153 */ supportsEnrollmentAPISchemaColumns()154 public boolean supportsEnrollmentAPISchemaColumns() { 155 return mDbVersion >= DATABASE_VERSION_V4; 156 } 157 158 /** Check whether db has all tables. */ hasAllTables(SQLiteDatabase db, String[] tableArray)159 public static boolean hasAllTables(SQLiteDatabase db, String[] tableArray) { 160 List<String> selectionArgList = new ArrayList<>(Arrays.asList(tableArray)); 161 selectionArgList.add("table"); // Schema type to match 162 String[] selectionArgs = new String[selectionArgList.size()]; 163 selectionArgList.toArray(selectionArgs); 164 return DatabaseUtils.queryNumEntries( 165 db, 166 "sqlite_master", 167 "name IN (" 168 + Stream.generate(() -> "?") 169 .limit(tableArray.length) 170 .collect(Collectors.joining(",")) 171 + ")" 172 + " AND type = ?", 173 selectionArgs) 174 == tableArray.length; 175 } 176 177 /** Wraps getWritableDatabase to catch SQLiteException and log error. */ 178 @Nullable safeGetWritableDatabase()179 public SQLiteDatabase safeGetWritableDatabase() { 180 try { 181 return super.getWritableDatabase(); 182 } catch (SQLiteException e) { 183 sLogger.e(e, "Failed to get a writeable database"); 184 ErrorLogUtil.e( 185 e, 186 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATABASE_WRITE_EXCEPTION, 187 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 188 return null; 189 } 190 } 191 192 /** Wraps getReadableDatabase to catch SQLiteException and log error. */ 193 @Nullable safeGetReadableDatabase()194 public SQLiteDatabase safeGetReadableDatabase() { 195 try { 196 return super.getReadableDatabase(); 197 } catch (SQLiteException e) { 198 sLogger.e(e, "Failed to get a readable database"); 199 ErrorLogUtil.e( 200 e, 201 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATABASE_READ_EXCEPTION, 202 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 203 return null; 204 } 205 } 206 createSchema(SQLiteDatabase db)207 private void createSchema(SQLiteDatabase db) { 208 if (FlagsFactory.getFlags().getSharedDatabaseSchemaVersion4Enabled()) { 209 EnrollmentTables.CREATE_STATEMENTS_V2.forEach(db::execSQL); 210 } else { 211 EnrollmentTables.CREATE_STATEMENTS_V1.forEach(db::execSQL); 212 } 213 214 EncryptionKeyTables.CREATE_STATEMENTS_V3.forEach(db::execSQL); 215 } 216 createEnrollmentV1Schema(SQLiteDatabase db)217 private void createEnrollmentV1Schema(SQLiteDatabase db) { 218 EnrollmentTables.CREATE_STATEMENTS_V1.forEach(db::execSQL); 219 } 220 createEncryptionKeyV2Schema(SQLiteDatabase db)221 private void createEncryptionKeyV2Schema(SQLiteDatabase db) { 222 EncryptionKeyTables.CREATE_STATEMENTS_V2.forEach(db::execSQL); 223 } 224 migrateOldDataToNewDatabase( SQLiteDatabase oldDb, SQLiteDatabase db, String[] tables)225 private void migrateOldDataToNewDatabase( 226 SQLiteDatabase oldDb, SQLiteDatabase db, String[] tables) { 227 // Ordered iteration to populate tables to avoid 228 // foreign key constraint failures. 229 Arrays.stream(tables).forEachOrdered((table) -> copyOrMigrateTable(oldDb, db, table)); 230 } 231 copyOrMigrateTable(SQLiteDatabase oldDb, SQLiteDatabase newDb, String table)232 private void copyOrMigrateTable(SQLiteDatabase oldDb, SQLiteDatabase newDb, String table) { 233 // We are moving from Origin-Based Enrollment to Site-Based Enrollment as part of this 234 // migration 235 // 236 switch (table) { 237 case EnrollmentTables.EnrollmentDataContract.TABLE: 238 migrateEnrollmentTable(oldDb, newDb, table); 239 return; 240 default: 241 copyTable(oldDb, newDb, table); 242 } 243 } 244 copyTable(SQLiteDatabase oldDb, SQLiteDatabase newDb, String table)245 private void copyTable(SQLiteDatabase oldDb, SQLiteDatabase newDb, String table) { 246 try (Cursor cursor = oldDb.query(table, null, null, null, null, null, null, null)) { 247 while (cursor.moveToNext()) { 248 ContentValues contentValues = new ContentValues(); 249 DatabaseUtils.cursorRowToContentValues(cursor, contentValues); 250 newDb.insert(table, null, contentValues); 251 } 252 } 253 } 254 migrateEnrollmentTable(SQLiteDatabase oldDb, SQLiteDatabase newDb, String table)255 private void migrateEnrollmentTable(SQLiteDatabase oldDb, SQLiteDatabase newDb, String table) { 256 try (Cursor cursor = oldDb.query(table, null, null, null, null, null, null, null)) { 257 // Enrollment table is moving to Site based enrollment. 258 // We are filtering out records with duplicated sites. 259 Set<Uri> duplicateSites = new HashSet<>(); 260 Map<Uri, ContentValues> siteToEnrollmentMap = new HashMap<>(); 261 while (cursor.moveToNext()) { 262 Optional<Uri> site = tryGetSiteFromEnrollmentRow(cursor); 263 ContentValues contentValues = new ContentValues(); 264 DatabaseUtils.cursorRowToContentValues(cursor, contentValues); 265 if (site.isEmpty()) { 266 newDb.insert(table, null, contentValues); 267 } else if (siteToEnrollmentMap.containsKey(site.get())) { 268 duplicateSites.add(site.get()); 269 } else { 270 siteToEnrollmentMap.put(site.get(), contentValues); 271 } 272 } 273 siteToEnrollmentMap.forEach( 274 (site, enrollment) -> { 275 if (duplicateSites.contains(site)) { 276 return; 277 } 278 newDb.insert(table, null, enrollment); 279 }); 280 } 281 } 282 tryGetSiteFromEnrollmentRow(Cursor cursor)283 private Optional<Uri> tryGetSiteFromEnrollmentRow(Cursor cursor) { 284 try { 285 EnrollmentData enrollmentData = 286 SqliteObjectMapper.constructEnrollmentDataFromCursor(cursor); 287 Uri uri = Uri.parse(enrollmentData.getAttributionReportingUrl().get(0)); 288 return WebAddresses.topPrivateDomainAndScheme(uri); 289 } catch (IndexOutOfBoundsException ex) { 290 return Optional.empty(); 291 } 292 } 293 upgradeSchema(SQLiteDatabase db, int oldVersion, int newVersion)294 private void upgradeSchema(SQLiteDatabase db, int oldVersion, int newVersion) { 295 sLogger.d( 296 "SharedDbHelper.upgradeToLatestSchema. " 297 + "Attempting to upgrade version from %d to %d.", 298 oldVersion, newVersion); 299 getOrderedDbMigrators() 300 .forEach(dbMigrator -> dbMigrator.performMigration(db, oldVersion, newVersion)); 301 } 302 303 // Get the database version to create. It may be different depending on Flags status. getDatabaseVersionToCreate()304 static int getDatabaseVersionToCreate() { 305 return FlagsFactory.getFlags().getSharedDatabaseSchemaVersion4Enabled() 306 ? DATABASE_VERSION_V4 307 : DATABASE_VERSION_V3; 308 } 309 } 310