1 /* 2 * Copyright (C) 2022 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; 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.Context; 25 import android.database.DatabaseUtils; 26 import android.database.sqlite.SQLiteDatabase; 27 import android.database.sqlite.SQLiteException; 28 import android.database.sqlite.SQLiteOpenHelper; 29 30 import com.android.adservices.LogUtil; 31 import com.android.adservices.data.enrollment.EnrollmentTables; 32 import com.android.adservices.data.measurement.MeasurementTables; 33 import com.android.adservices.data.measurement.migration.IMeasurementDbMigrator; 34 import com.android.adservices.data.measurement.migration.MeasurementDbMigratorV2; 35 import com.android.adservices.data.measurement.migration.MeasurementDbMigratorV3; 36 import com.android.adservices.data.measurement.migration.MeasurementDbMigratorV6; 37 import com.android.adservices.data.topics.TopicsTables; 38 import com.android.adservices.data.topics.migration.ITopicsDbMigrator; 39 import com.android.adservices.data.topics.migration.TopicsDbMigratorV7; 40 import com.android.adservices.data.topics.migration.TopicsDbMigratorV8; 41 import com.android.adservices.data.topics.migration.TopicsDbMigratorV9; 42 import com.android.adservices.errorlogging.ErrorLogUtil; 43 import com.android.adservices.service.FlagsFactory; 44 import com.android.adservices.service.common.compat.FileCompatUtils; 45 import com.android.adservices.shared.common.ApplicationContextSingleton; 46 import com.android.internal.annotations.VisibleForTesting; 47 48 import com.google.common.collect.ImmutableList; 49 50 import java.io.File; 51 import java.util.ArrayList; 52 import java.util.Arrays; 53 import java.util.List; 54 import java.util.stream.Collectors; 55 import java.util.stream.Stream; 56 57 /** 58 * Helper to manage the PP API database. Designed as a singleton to make sure that all PP API usages 59 * get the same reference. 60 */ 61 public class DbHelper extends SQLiteOpenHelper { 62 // Version 9: Add new table ReturnedEncryptedTopic, guarded by feature flag. 63 public static final int DATABASE_VERSION_V9 = 9; 64 // Version 8: Add logged_topic to ReturnedTopic table for Topics API, guarded by feature flag. 65 public static final int DATABASE_VERSION_V8 = 8; 66 67 public static final int DATABASE_VERSION_7 = 7; 68 69 private static final String DATABASE_NAME = 70 FileCompatUtils.getAdservicesFilename("adservices.db"); 71 72 private static DbHelper sSingleton = null; 73 private final File mDbFile; 74 // The version when the database is actually created 75 private final int mDbVersion; 76 77 /** 78 * It's only public to unit test. 79 * 80 * @param context the context 81 * @param dbName Name of database to query 82 * @param dbVersion db version 83 */ 84 @VisibleForTesting DbHelper(Context context, String dbName, int dbVersion)85 public DbHelper(Context context, String dbName, int dbVersion) { 86 super(context, dbName, null, dbVersion); 87 mDbFile = FileCompatUtils.getDatabasePathHelper(context, dbName); 88 this.mDbVersion = dbVersion; 89 } 90 91 /** Returns an instance of the DbHelper given a context. */ getInstance()92 public static DbHelper getInstance() { 93 synchronized (DbHelper.class) { 94 if (sSingleton == null) { 95 sSingleton = 96 new DbHelper( 97 ApplicationContextSingleton.get(), 98 DATABASE_NAME, 99 getDatabaseVersionToCreate()); 100 } 101 return sSingleton; 102 } 103 } 104 105 @Override onCreate(SQLiteDatabase db)106 public void onCreate(SQLiteDatabase db) { 107 LogUtil.d("DbHelper.onCreate with version %d. Name: %s", mDbVersion, mDbFile.getName()); 108 for (String sql : TopicsTables.CREATE_STATEMENTS) { 109 db.execSQL(sql); 110 } 111 for (String sql : EnrollmentTables.CREATE_STATEMENTS) { 112 db.execSQL(sql); 113 } 114 } 115 116 @Override onOpen(SQLiteDatabase db)117 public void onOpen(SQLiteDatabase db) { 118 super.onOpen(db); 119 db.execSQL("PRAGMA foreign_keys=ON"); 120 } 121 122 /** Wraps getReadableDatabase to catch SQLiteException and log error. */ 123 @Nullable safeGetReadableDatabase()124 public SQLiteDatabase safeGetReadableDatabase() { 125 try { 126 return super.getReadableDatabase(); 127 } catch (SQLiteException e) { 128 LogUtil.e(e, "Failed to get a readable database"); 129 ErrorLogUtil.e( 130 e, 131 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATABASE_READ_EXCEPTION, 132 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 133 return null; 134 } 135 } 136 137 /** Wraps getWritableDatabase to catch SQLiteException and log error. */ 138 @Nullable safeGetWritableDatabase()139 public SQLiteDatabase safeGetWritableDatabase() { 140 try { 141 return super.getWritableDatabase(); 142 } catch (SQLiteException e) { 143 LogUtil.e(e, "Failed to get a writeable database"); 144 ErrorLogUtil.e( 145 e, 146 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__DATABASE_WRITE_EXCEPTION, 147 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON); 148 return null; 149 } 150 } 151 152 // TODO(b/255964885): Consolidate DB Migrator Class across Rubidium 153 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)154 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 155 LogUtil.d( 156 "DbHelper.onUpgrade. Attempting to upgrade version from %d to %d.", 157 oldVersion, newVersion); 158 // Apply migrations only if all V1 tables are present, otherwise skip migration because 159 // either Db is in an unexpected stage or Measurement has started using its own database. 160 // Note: This check works because there is no table deletion from V1 to V6. 161 if (hasV1MeasurementTables(db)) { 162 getOrderedDbMigrators() 163 .forEach(dbMigrator -> dbMigrator.performMigration(db, oldVersion, newVersion)); 164 } 165 try { 166 topicsGetOrderedDbMigrators() 167 .forEach(dbMigrator -> dbMigrator.performMigration(db, oldVersion, newVersion)); 168 } catch (IllegalArgumentException e) { 169 LogUtil.e( 170 "Topics DB Upgrade is not performed! oldVersion: %d, newVersion: %d.", 171 oldVersion, newVersion); 172 } 173 } 174 175 /** Check if V1 measurement tables exist. */ 176 @VisibleForTesting hasV1MeasurementTables(SQLiteDatabase db)177 public boolean hasV1MeasurementTables(SQLiteDatabase db) { 178 List<String> selectionArgList = new ArrayList<>(Arrays.asList(MeasurementTables.V1_TABLES)); 179 selectionArgList.add("table"); // Schema type to match 180 String[] selectionArgs = new String[selectionArgList.size()]; 181 selectionArgList.toArray(selectionArgs); 182 return DatabaseUtils.queryNumEntries( 183 db, 184 "sqlite_master", 185 "name IN (" 186 + Stream.generate(() -> "?") 187 .limit(MeasurementTables.V1_TABLES.length) 188 .collect(Collectors.joining(",")) 189 + ")" 190 + " AND type = ?", 191 selectionArgs) 192 == MeasurementTables.V1_TABLES.length; 193 } 194 195 @Override onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)196 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 197 LogUtil.d("Downgrade database version from %d to %d.", oldVersion, newVersion); 198 // prevent parent class to throw SQLiteException 199 } 200 getDbFileSize()201 public long getDbFileSize() { 202 return mDbFile != null && mDbFile.exists() ? mDbFile.length() : -1; 203 } 204 205 /** 206 * Check whether logged_topic column is supported in ReturnedTopic Table. This column is 207 * introduced in Version 8. 208 */ supportsLoggedTopicInReturnedTopicTable()209 public boolean supportsLoggedTopicInReturnedTopicTable() { 210 return mDbVersion >= DATABASE_VERSION_V8; 211 } 212 213 /** Get Migrators in order for Measurement. */ 214 @VisibleForTesting getOrderedDbMigrators()215 public List<IMeasurementDbMigrator> getOrderedDbMigrators() { 216 return ImmutableList.of( 217 new MeasurementDbMigratorV2(), 218 new MeasurementDbMigratorV3(), 219 new MeasurementDbMigratorV6()); 220 } 221 222 /** Get Migrators in order for Topics. */ 223 @VisibleForTesting topicsGetOrderedDbMigrators()224 public List<ITopicsDbMigrator> topicsGetOrderedDbMigrators() { 225 return ImmutableList.of( 226 new TopicsDbMigratorV7(), new TopicsDbMigratorV8(), new TopicsDbMigratorV9()); 227 } 228 229 // Get the database version to create. It may be different as DATABASE_VERSION, depending 230 // on Flags status. 231 @VisibleForTesting getDatabaseVersionToCreate()232 static int getDatabaseVersionToCreate() { 233 if (FlagsFactory.getFlags().getEnableDatabaseSchemaVersion9()) { 234 return DATABASE_VERSION_V9; 235 } else if (FlagsFactory.getFlags().getEnableDatabaseSchemaVersion8()) { 236 return DATABASE_VERSION_V8; 237 } else { 238 return DATABASE_VERSION_7; 239 } 240 } 241 } 242