• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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