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