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.topics;
18 
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_ALL_ENTRIES_IN_TABLE_FAILURE;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_BLOCKED_TOPICS_FAILURE;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_COLUMN_FAILURE;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_OLD_EPOCH_FAILURE;
23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_TABLE_FAILURE;
24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_PERSIST_CLASSIFIED_TOPICS_FAILURE;
25 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_PERSIST_TOPICS_CONTRIBUTORS_FAILURE;
26 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_PERSIST_TOP_TOPICS_FAILURE;
27 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_APP_SDK_USAGE_FAILURE;
28 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_APP_USAGE_FAILURE;
29 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_BLOCKED_TOPICS_FAILURE;
30 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_CAN_LEARN_TOPICS_FAILURE;
31 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_RETURNED_TOPICS_FAILURE;
32 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS;
33 
34 import android.content.ContentValues;
35 import android.database.Cursor;
36 import android.database.SQLException;
37 import android.database.sqlite.SQLiteDatabase;
38 import android.util.Pair;
39 
40 import com.android.adservices.LoggerFactory;
41 import com.android.adservices.data.DbHelper;
42 import com.android.adservices.errorlogging.ErrorLogUtil;
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.util.Preconditions;
45 
46 import java.util.ArrayList;
47 import java.util.Arrays;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Objects;
53 import java.util.Set;
54 
55 /** Data Access Object for the Topics API. */
56 public class TopicsDao {
57     private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger();
58     private static TopicsDao sSingleton;
59     private static final Object SINGLETON_LOCK = new Object();
60 
61     // Defined constants for error codes which have very long names
62     private static final int TOPICS_PERSIST_CLASSIFIED_TOPICS_FAILURE =
63             AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_PERSIST_CLASSIFIED_TOPICS_FAILURE;
64     private static final int TOPICS_RECORD_CAN_LEARN_TOPICS_FAILURE =
65             AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_CAN_LEARN_TOPICS_FAILURE;
66     private static final int TOPICS_RECORD_RETURNED_TOPICS_FAILURE =
67             AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_RETURNED_TOPICS_FAILURE;
68     private static final int TOPICS_PERSIST_TOPICS_CONTRIBUTORS_FAILURE =
69             AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_PERSIST_TOPICS_CONTRIBUTORS_FAILURE;
70     private static final int TOPICS_DELETE_ALL_ENTRIES_IN_TABLE_FAILURE =
71             AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_ALL_ENTRIES_IN_TABLE_FAILURE;
72 
73     private static final String[] ALL_TOPICS_TABLES = {
74         TopicsTables.TaxonomyContract.TABLE,
75         TopicsTables.AppClassificationTopicsContract.TABLE,
76         TopicsTables.AppUsageHistoryContract.TABLE,
77         TopicsTables.UsageHistoryContract.TABLE,
78         TopicsTables.CallerCanLearnTopicsContract.TABLE,
79         TopicsTables.ReturnedTopicContract.TABLE,
80         TopicsTables.ReturnedEncryptedTopicContract.TABLE,
81         TopicsTables.TopTopicsContract.TABLE,
82         TopicsTables.BlockedTopicsContract.TABLE,
83         TopicsTables.EpochOriginContract.TABLE,
84         TopicsTables.TopicContributorsContract.TABLE
85     };
86 
87     private final DbHelper mDbHelper; // Used in tests.
88 
89     /**
90      * It's only public to unit test.
91      *
92      * @param dbHelper The database to query
93      */
94     @VisibleForTesting
TopicsDao(DbHelper dbHelper)95     public TopicsDao(DbHelper dbHelper) {
96         mDbHelper = dbHelper;
97     }
98 
99     /** Returns an instance of the TopicsDAO given a context. */
getInstance()100     public static TopicsDao getInstance() {
101         synchronized (SINGLETON_LOCK) {
102             if (sSingleton == null) {
103                 sSingleton = new TopicsDao(DbHelper.getInstance());
104             }
105             return sSingleton;
106         }
107     }
108 
109     /**
110      * Persist the apps and their classification topics.
111      *
112      * @param epochId the epoch ID to persist
113      * @param appClassificationTopicsMap Map of app -> classified topics
114      */
persistAppClassificationTopics( long epochId, Map<String, List<Topic>> appClassificationTopicsMap)115     public void persistAppClassificationTopics(
116             long epochId, Map<String, List<Topic>> appClassificationTopicsMap) {
117         Objects.requireNonNull(appClassificationTopicsMap);
118 
119         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
120         if (db == null) {
121             return;
122         }
123 
124         for (Map.Entry<String, List<Topic>> entry : appClassificationTopicsMap.entrySet()) {
125             String app = entry.getKey();
126 
127             // save each topic in the list by app -> topic mapping in the DB
128             for (Topic topic : entry.getValue()) {
129                 ContentValues values = new ContentValues();
130                 values.put(TopicsTables.AppClassificationTopicsContract.EPOCH_ID, epochId);
131                 values.put(TopicsTables.AppClassificationTopicsContract.APP, app);
132                 values.put(
133                         TopicsTables.AppClassificationTopicsContract.TAXONOMY_VERSION,
134                         topic.getTaxonomyVersion());
135                 values.put(
136                         TopicsTables.AppClassificationTopicsContract.MODEL_VERSION,
137                         topic.getModelVersion());
138                 values.put(TopicsTables.AppClassificationTopicsContract.TOPIC, topic.getTopic());
139 
140                 try {
141                     db.insert(
142                             TopicsTables.AppClassificationTopicsContract.TABLE,
143                             /* nullColumnHack */ null,
144                             values);
145                 } catch (SQLException e) {
146                     sLogger.e("Failed to persist classified Topics. Exception : " + e.getMessage());
147                     ErrorLogUtil.e(
148                             e,
149                             TOPICS_PERSIST_CLASSIFIED_TOPICS_FAILURE,
150                             AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
151                 }
152             }
153         }
154     }
155 
156     /**
157      * Get the map of apps and their classification topics.
158      *
159      * @param epochId the epoch ID to retrieve
160      * @return {@link Map} a map of app -> topics
161      */
retrieveAppClassificationTopics(long epochId)162     public Map<String, List<Topic>> retrieveAppClassificationTopics(long epochId) {
163         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
164         Map<String, List<Topic>> appTopicsMap = new HashMap<>();
165         if (db == null) {
166             return appTopicsMap;
167         }
168 
169         String[] projection = {
170             TopicsTables.AppClassificationTopicsContract.APP,
171             TopicsTables.AppClassificationTopicsContract.TAXONOMY_VERSION,
172             TopicsTables.AppClassificationTopicsContract.MODEL_VERSION,
173             TopicsTables.AppClassificationTopicsContract.TOPIC,
174         };
175 
176         String selection = TopicsTables.AppClassificationTopicsContract.EPOCH_ID + " = ?";
177         String[] selectionArgs = {String.valueOf(epochId)};
178 
179         try (Cursor cursor =
180                 db.query(
181                         TopicsTables.AppClassificationTopicsContract.TABLE, // The table to query
182                         projection, // The array of columns to return (pass null to get all)
183                         selection, // The columns for the WHERE clause
184                         selectionArgs, // The values for the WHERE clause
185                         null, // don't group the rows
186                         null, // don't filter by row groups
187                         null // The sort order
188                         )) {
189             while (cursor.moveToNext()) {
190                 String app =
191                         cursor.getString(
192                                 cursor.getColumnIndexOrThrow(
193                                         TopicsTables.AppClassificationTopicsContract.APP));
194                 long taxonomyVersion =
195                         cursor.getLong(
196                                 cursor.getColumnIndexOrThrow(
197                                         TopicsTables.AppClassificationTopicsContract
198                                                 .TAXONOMY_VERSION));
199                 long modelVersion =
200                         cursor.getLong(
201                                 cursor.getColumnIndexOrThrow(
202                                         TopicsTables.AppClassificationTopicsContract
203                                                 .MODEL_VERSION));
204                 int topicId =
205                         cursor.getInt(
206                                 cursor.getColumnIndexOrThrow(
207                                         TopicsTables.AppClassificationTopicsContract.TOPIC));
208                 Topic topic = Topic.create(topicId, taxonomyVersion, modelVersion);
209 
210                 List<Topic> list = appTopicsMap.getOrDefault(app, new ArrayList<>());
211                 list.add(topic);
212                 appTopicsMap.put(app, list);
213             }
214         }
215 
216         return appTopicsMap;
217     }
218 
219     /**
220      * Persist the list of Top Topics in this epoch to DB.
221      *
222      * @param epochId ID of current epoch
223      * @param topTopics the topics list to persist into DB
224      */
persistTopTopics(long epochId, List<Topic> topTopics)225     public void persistTopTopics(long epochId, List<Topic> topTopics) {
226         // topTopics the Top Topics: a list of 5 top topics and the 6th topic
227         // which was selected randomly. We can refer this 6th topic as the random-topic.
228         Objects.requireNonNull(topTopics);
229         Preconditions.checkArgument(topTopics.size() == 6);
230 
231         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
232         if (db == null) {
233             return;
234         }
235 
236         ContentValues values = new ContentValues();
237         values.put(TopicsTables.TopTopicsContract.EPOCH_ID, epochId);
238         values.put(TopicsTables.TopTopicsContract.TOPIC1, topTopics.get(0).getTopic());
239         values.put(TopicsTables.TopTopicsContract.TOPIC2, topTopics.get(1).getTopic());
240         values.put(TopicsTables.TopTopicsContract.TOPIC3, topTopics.get(2).getTopic());
241         values.put(TopicsTables.TopTopicsContract.TOPIC4, topTopics.get(3).getTopic());
242         values.put(TopicsTables.TopTopicsContract.TOPIC5, topTopics.get(4).getTopic());
243         values.put(TopicsTables.TopTopicsContract.RANDOM_TOPIC, topTopics.get(5).getTopic());
244         // Taxonomy version and model version of all top topics should be the same.
245         // Therefore, get it from the first top topic.
246         values.put(
247                 TopicsTables.TopTopicsContract.TAXONOMY_VERSION,
248                 topTopics.get(0).getTaxonomyVersion());
249         values.put(
250                 TopicsTables.TopTopicsContract.MODEL_VERSION, topTopics.get(0).getModelVersion());
251 
252         try {
253             db.insert(TopicsTables.TopTopicsContract.TABLE, /* nullColumnHack */ null, values);
254         } catch (SQLException e) {
255             sLogger.e("Failed to persist Top Topics. Exception : " + e.getMessage());
256             ErrorLogUtil.e(
257                     e,
258                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_PERSIST_TOP_TOPICS_FAILURE,
259                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
260         }
261     }
262 
263     /**
264      * Return the Top Topics. This will retrieve a list of 5 top topics and the 6th random topic
265      * from DB.
266      *
267      * @param epochId the epochId to retrieve the top topics.
268      * @return {@link List} a {@link List} of {@link Topic}
269      */
retrieveTopTopics(long epochId)270     public List<Topic> retrieveTopTopics(long epochId) {
271         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
272         if (db == null) {
273             return new ArrayList<>();
274         }
275 
276         String[] projection = {
277             TopicsTables.TopTopicsContract.TOPIC1,
278             TopicsTables.TopTopicsContract.TOPIC2,
279             TopicsTables.TopTopicsContract.TOPIC3,
280             TopicsTables.TopTopicsContract.TOPIC4,
281             TopicsTables.TopTopicsContract.TOPIC5,
282             TopicsTables.TopTopicsContract.RANDOM_TOPIC,
283             TopicsTables.TopTopicsContract.TAXONOMY_VERSION,
284             TopicsTables.TopTopicsContract.MODEL_VERSION
285         };
286 
287         String selection = TopicsTables.AppClassificationTopicsContract.EPOCH_ID + " = ?";
288         String[] selectionArgs = {String.valueOf(epochId)};
289 
290         try (Cursor cursor =
291                 db.query(
292                         TopicsTables.TopTopicsContract.TABLE, // The table to query
293                         projection, // The array of columns to return (pass null to get all)
294                         selection, // The columns for the WHERE clause
295                         selectionArgs, // The values for the WHERE clause
296                         null, // don't group the rows
297                         null, // don't filter by row groups
298                         null // The sort order
299                         )) {
300             if (cursor.moveToNext()) {
301                 int topicId1 =
302                         cursor.getInt(
303                                 cursor.getColumnIndexOrThrow(
304                                         TopicsTables.TopTopicsContract.TOPIC1));
305                 int topicId2 =
306                         cursor.getInt(
307                                 cursor.getColumnIndexOrThrow(
308                                         TopicsTables.TopTopicsContract.TOPIC2));
309                 int topicId3 =
310                         cursor.getInt(
311                                 cursor.getColumnIndexOrThrow(
312                                         TopicsTables.TopTopicsContract.TOPIC3));
313                 int topicId4 =
314                         cursor.getInt(
315                                 cursor.getColumnIndexOrThrow(
316                                         TopicsTables.TopTopicsContract.TOPIC4));
317                 int topicId5 =
318                         cursor.getInt(
319                                 cursor.getColumnIndexOrThrow(
320                                         TopicsTables.TopTopicsContract.TOPIC5));
321                 int randomTopicId =
322                         cursor.getInt(
323                                 cursor.getColumnIndexOrThrow(
324                                         TopicsTables.TopTopicsContract.RANDOM_TOPIC));
325                 long taxonomyVersion =
326                         cursor.getLong(
327                                 cursor.getColumnIndexOrThrow(
328                                         TopicsTables.TopTopicsContract.TAXONOMY_VERSION));
329                 long modelVersion =
330                         cursor.getLong(
331                                 cursor.getColumnIndexOrThrow(
332                                         TopicsTables.TopTopicsContract.MODEL_VERSION));
333                 Topic topic1 = Topic.create(topicId1, taxonomyVersion, modelVersion);
334                 Topic topic2 = Topic.create(topicId2, taxonomyVersion, modelVersion);
335                 Topic topic3 = Topic.create(topicId3, taxonomyVersion, modelVersion);
336                 Topic topic4 = Topic.create(topicId4, taxonomyVersion, modelVersion);
337                 Topic topic5 = Topic.create(topicId5, taxonomyVersion, modelVersion);
338                 Topic randomTopic = Topic.create(randomTopicId, taxonomyVersion, modelVersion);
339                 return Arrays.asList(topic1, topic2, topic3, topic4, topic5, randomTopic);
340             }
341         }
342 
343         return new ArrayList<>();
344     }
345 
346     /**
347      * Record the App and SDK into the Usage History table.
348      *
349      * @param epochId epochId epoch id to record
350      * @param app app name
351      * @param sdk sdk name
352      */
recordUsageHistory(long epochId, String app, String sdk)353     public void recordUsageHistory(long epochId, String app, String sdk) {
354         Objects.requireNonNull(app);
355         Objects.requireNonNull(sdk);
356         Preconditions.checkStringNotEmpty(app);
357         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
358         if (db == null) {
359             return;
360         }
361 
362         // Create a new map of values, where column names are the keys
363         ContentValues values = new ContentValues();
364         values.put(TopicsTables.UsageHistoryContract.APP, app);
365         values.put(TopicsTables.UsageHistoryContract.SDK, sdk);
366         values.put(TopicsTables.UsageHistoryContract.EPOCH_ID, epochId);
367 
368         try {
369             db.insert(TopicsTables.UsageHistoryContract.TABLE, /* nullColumnHack */ null, values);
370         } catch (SQLException e) {
371             sLogger.e("Failed to record App-Sdk usage history." + e.getMessage());
372             ErrorLogUtil.e(
373                     e,
374                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_APP_SDK_USAGE_FAILURE,
375                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
376         }
377 
378     }
379 
380     /**
381      * Record the usage history for app only
382      *
383      * @param epochId epoch id to record
384      * @param app app name
385      */
recordAppUsageHistory(long epochId, String app)386     public void recordAppUsageHistory(long epochId, String app) {
387         Objects.requireNonNull(app);
388         Preconditions.checkStringNotEmpty(app);
389         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
390         if (db == null) {
391             return;
392         }
393 
394         // Create a new map of values, where column names are the keys
395         ContentValues values = new ContentValues();
396         values.put(TopicsTables.AppUsageHistoryContract.APP, app);
397         values.put(TopicsTables.AppUsageHistoryContract.EPOCH_ID, epochId);
398 
399         try {
400             db.insert(
401                     TopicsTables.AppUsageHistoryContract.TABLE, /* nullColumnHack */ null, values);
402         } catch (SQLException e) {
403             sLogger.e("Failed to record App Only usage history." + e.getMessage());
404             ErrorLogUtil.e(
405                     e,
406                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_APP_USAGE_FAILURE,
407                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
408         }
409     }
410 
411     /**
412      * Return all apps and their SDKs that called Topics API in the epoch.
413      *
414      * @param epochId the epoch to retrieve the app and sdk usage for.
415      * @return Return Map<App, List<SDK>>.
416      */
retrieveAppSdksUsageMap(long epochId)417     public Map<String, List<String>> retrieveAppSdksUsageMap(long epochId) {
418         Map<String, List<String>> appSdksUsageMap = new HashMap<>();
419         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
420         if (db == null) {
421             return appSdksUsageMap;
422         }
423 
424         String[] projection = {
425             TopicsTables.UsageHistoryContract.APP, TopicsTables.UsageHistoryContract.SDK,
426         };
427 
428         String selection = TopicsTables.UsageHistoryContract.EPOCH_ID + " = ?";
429         String[] selectionArgs = {String.valueOf(epochId)};
430 
431         try (Cursor cursor =
432                 db.query(
433                         /* distinct= */ true,
434                         TopicsTables.UsageHistoryContract.TABLE,
435                         projection,
436                         selection,
437                         selectionArgs,
438                         null,
439                         null,
440                         null,
441                         null)) {
442             while (cursor.moveToNext()) {
443                 String app =
444                         cursor.getString(
445                                 cursor.getColumnIndexOrThrow(
446                                         TopicsTables.UsageHistoryContract.APP));
447                 String sdk =
448                         cursor.getString(
449                                 cursor.getColumnIndexOrThrow(
450                                         TopicsTables.UsageHistoryContract.SDK));
451                 if (!appSdksUsageMap.containsKey(app)) {
452                     appSdksUsageMap.put(app, new ArrayList<>());
453                 }
454                 appSdksUsageMap.get(app).add(sdk);
455             }
456         }
457 
458         return appSdksUsageMap;
459     }
460 
461     /**
462      * Get topic api usage of an app in an epoch.
463      *
464      * @param epochId the epoch to retrieve the app usage for.
465      * @return Map<App, UsageCount>, how many times an app called topics API in this epoch
466      */
retrieveAppUsageMap(long epochId)467     public Map<String, Integer> retrieveAppUsageMap(long epochId) {
468         Map<String, Integer> appUsageMap = new HashMap<>();
469         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
470         if (db == null) {
471             return appUsageMap;
472         }
473 
474         String[] projection = {
475             TopicsTables.AppUsageHistoryContract.APP,
476         };
477 
478         String selection = TopicsTables.AppUsageHistoryContract.EPOCH_ID + " = ?";
479         String[] selectionArgs = {String.valueOf(epochId)};
480 
481         try (Cursor cursor =
482                 db.query(
483                         TopicsTables.AppUsageHistoryContract.TABLE,
484                         projection,
485                         selection,
486                         selectionArgs,
487                         null,
488                         null,
489                         null,
490                         null)) {
491             while (cursor.moveToNext()) {
492                 String app =
493                         cursor.getString(
494                                 cursor.getColumnIndexOrThrow(
495                                         TopicsTables.AppUsageHistoryContract.APP));
496                 appUsageMap.put(app, appUsageMap.getOrDefault(app, 0) + 1);
497             }
498         }
499 
500         return appUsageMap;
501     }
502 
503     /**
504      * Get a union set of distinct apps among tables.
505      *
506      * @param tableNames a {@link List} of table names
507      * @param appColumnNames a {@link List} of app Column names for given tables
508      * @return a {@link Set} of unique apps in the table
509      * @throws IllegalArgumentException if {@code tableNames} and {@code appColumnNames} have
510      *     different sizes.
511      */
retrieveDistinctAppsFromTables( List<String> tableNames, List<String> appColumnNames)512     public Set<String> retrieveDistinctAppsFromTables(
513             List<String> tableNames, List<String> appColumnNames) {
514         Preconditions.checkArgument(tableNames.size() == appColumnNames.size());
515 
516         Set<String> apps = new HashSet<>();
517         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
518         if (db == null) {
519             return apps;
520         }
521 
522         for (int index = 0; index < tableNames.size(); index++) {
523             String[] projection = {appColumnNames.get(index)};
524 
525             try (Cursor cursor =
526                     db.query(
527                             /* distinct */ true,
528                             tableNames.get(index),
529                             projection,
530                             null,
531                             null,
532                             null,
533                             null,
534                             null,
535                             null)) {
536                 while (cursor.moveToNext()) {
537                     String app =
538                             cursor.getString(
539                                     cursor.getColumnIndexOrThrow(appColumnNames.get(index)));
540                     apps.add(app);
541                 }
542             }
543         }
544 
545         return apps;
546     }
547 
548     // TODO(b/236764602): Create a Caller Class.
549     /**
550      * Persist the Callers can learn topic map to DB.
551      *
552      * @param epochId the epoch ID.
553      * @param callerCanLearnMap callerCanLearnMap = {@code Map<Topic, Set<Caller>>} This is a Map
554      *     from Topic to set of App or Sdk (Caller = App or Sdk) that can learn about that topic.
555      *     This is similar to the table Can Learn Topic in the explainer.
556      */
persistCallerCanLearnTopics( long epochId, Map<Topic, Set<String>> callerCanLearnMap)557     public void persistCallerCanLearnTopics(
558             long epochId, Map<Topic, Set<String>> callerCanLearnMap) {
559         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
560         if (db == null) {
561             return;
562         }
563 
564         for (Map.Entry<Topic, Set<String>> entry : callerCanLearnMap.entrySet()) {
565             Topic topic = entry.getKey();
566             Set<String> callers = entry.getValue();
567 
568             for (String caller : callers) {
569                 ContentValues values = new ContentValues();
570                 values.put(TopicsTables.CallerCanLearnTopicsContract.CALLER, caller);
571                 values.put(TopicsTables.CallerCanLearnTopicsContract.TOPIC, topic.getTopic());
572                 values.put(TopicsTables.CallerCanLearnTopicsContract.EPOCH_ID, epochId);
573                 values.put(
574                         TopicsTables.CallerCanLearnTopicsContract.TAXONOMY_VERSION,
575                         topic.getTaxonomyVersion());
576                 values.put(
577                         TopicsTables.CallerCanLearnTopicsContract.MODEL_VERSION,
578                         topic.getModelVersion());
579 
580                 try {
581                     db.insert(
582                             TopicsTables.CallerCanLearnTopicsContract.TABLE,
583                             /* nullColumnHack */ null,
584                             values);
585                 } catch (SQLException e) {
586                     sLogger.e(e, "Failed to record can learn topic.");
587                     ErrorLogUtil.e(
588                             e,
589                             TOPICS_RECORD_CAN_LEARN_TOPICS_FAILURE,
590                             AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
591                 }
592             }
593         }
594     }
595 
596     /**
597      * Retrieve the CallersCanLearnTopicsMap This is a Map from Topic to set of App or Sdk (Caller =
598      * App or Sdk) that can learn about that topic. This is similar to the table Can Learn Topic in
599      * the explainer. We will look back numberOfLookBackEpochs epochs. The current explainer uses 3
600      * past epochs. Basically we select epochId between [epochId - numberOfLookBackEpochs + 1,
601      * epochId]
602      *
603      * @param epochId the epochId
604      * @param numberOfLookBackEpochs Look back numberOfLookBackEpochs.
605      * @return {@link Map} a Map<Topic, Set<Caller>> where Caller = App or Sdk.
606      */
retrieveCallerCanLearnTopicsMap( long epochId, int numberOfLookBackEpochs)607     public Map<Topic, Set<String>> retrieveCallerCanLearnTopicsMap(
608             long epochId, int numberOfLookBackEpochs) {
609         Preconditions.checkArgumentPositive(
610                 numberOfLookBackEpochs, "numberOfLookBackEpochs must be positive!");
611 
612         Map<Topic, Set<String>> callerCanLearnMap = new HashMap<>();
613         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
614         if (db == null) {
615             return callerCanLearnMap;
616         }
617 
618         String[] projection = {
619             TopicsTables.CallerCanLearnTopicsContract.CALLER,
620             TopicsTables.CallerCanLearnTopicsContract.TOPIC,
621             TopicsTables.CallerCanLearnTopicsContract.TAXONOMY_VERSION,
622             TopicsTables.CallerCanLearnTopicsContract.MODEL_VERSION,
623         };
624 
625         // Select epochId between [epochId - numberOfLookBackEpochs + 1, epochId]
626         String selection =
627                 " ? <= "
628                         + TopicsTables.CallerCanLearnTopicsContract.EPOCH_ID
629                         + " AND "
630                         + TopicsTables.CallerCanLearnTopicsContract.EPOCH_ID
631                         + " <= ?";
632         String[] selectionArgs = {
633             String.valueOf(epochId - numberOfLookBackEpochs + 1), String.valueOf(epochId)
634         };
635 
636         try (Cursor cursor =
637                 db.query(
638                         /* distinct= */ true,
639                         TopicsTables.CallerCanLearnTopicsContract.TABLE,
640                         projection,
641                         selection,
642                         selectionArgs,
643                         null,
644                         null,
645                         null,
646                         null)) {
647             if (cursor == null) {
648                 return callerCanLearnMap;
649             }
650 
651             while (cursor.moveToNext()) {
652                 String caller =
653                         cursor.getString(
654                                 cursor.getColumnIndexOrThrow(
655                                         TopicsTables.CallerCanLearnTopicsContract.CALLER));
656                 int topicId =
657                         cursor.getInt(
658                                 cursor.getColumnIndexOrThrow(
659                                         TopicsTables.CallerCanLearnTopicsContract.TOPIC));
660                 long taxonomyVersion =
661                         cursor.getLong(
662                                 cursor.getColumnIndexOrThrow(
663                                         TopicsTables.CallerCanLearnTopicsContract
664                                                 .TAXONOMY_VERSION));
665                 long modelVersion =
666                         cursor.getLong(
667                                 cursor.getColumnIndexOrThrow(
668                                         TopicsTables.CallerCanLearnTopicsContract.MODEL_VERSION));
669                 Topic topic = Topic.create(topicId, taxonomyVersion, modelVersion);
670                 if (!callerCanLearnMap.containsKey(topic)) {
671                     callerCanLearnMap.put(topic, new HashSet<>());
672                 }
673                 callerCanLearnMap.get(topic).add(caller);
674             }
675         }
676 
677         return callerCanLearnMap;
678     }
679 
680     // TODO(b/236759629): Add a validation to ensure same topic for an app.
681 
682     /**
683      * Persist the Apps, Sdks returned topics to DB.
684      *
685      * @param epochId the epoch ID
686      * @param returnedAppSdkTopics {@link Map} a Map<Pair<app, sdk>, Topic>
687      */
persistReturnedAppTopicsMap( long epochId, Map<Pair<String, String>, Topic> returnedAppSdkTopics)688     public void persistReturnedAppTopicsMap(
689             long epochId, Map<Pair<String, String>, Topic> returnedAppSdkTopics) {
690         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
691         if (db == null) {
692             return;
693         }
694 
695         for (Map.Entry<Pair<String, String>, Topic> app : returnedAppSdkTopics.entrySet()) {
696             // Entry: Key = <Pair<App, Sdk>, Value = Topic.
697             ContentValues values = new ContentValues();
698             values.put(TopicsTables.ReturnedTopicContract.EPOCH_ID, epochId);
699             values.put(TopicsTables.ReturnedTopicContract.APP, app.getKey().first);
700             values.put(TopicsTables.ReturnedTopicContract.SDK, app.getKey().second);
701             values.put(TopicsTables.ReturnedTopicContract.TOPIC, app.getValue().getTopic());
702             values.put(
703                     TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION,
704                     app.getValue().getTaxonomyVersion());
705             values.put(
706                     TopicsTables.ReturnedTopicContract.MODEL_VERSION,
707                     app.getValue().getModelVersion());
708             // Persist the logged topic to DB if ENABLE_LOGGED_TOPIC is true.
709             if (supportsLoggedTopicInReturnedTopicTable()) {
710                 values.put(TopicsTables.ReturnedTopicContract.LOGGED_TOPIC,
711                         app.getValue().getLoggedTopic());
712             }
713 
714             try {
715                 db.insert(
716                         TopicsTables.ReturnedTopicContract.TABLE,
717                         /* nullColumnHack */ null,
718                         values);
719             } catch (SQLException e) {
720                 sLogger.e(e, "Failed to record returned topic.");
721                 ErrorLogUtil.e(
722                         e,
723                         TOPICS_RECORD_RETURNED_TOPICS_FAILURE,
724                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
725             }
726         }
727     }
728 
729     /**
730      * Persist the Apps, Sdks returned topics to DB.
731      *
732      * @param epochId the epoch ID
733      * @param returnedAppSdkEncryptedTopics {@link Map} a Map< Pair< app, sdk>, EncryptedTopic>
734      */
persistReturnedAppEncryptedTopicsMap( long epochId, Map<Pair<String, String>, EncryptedTopic> returnedAppSdkEncryptedTopics)735     public void persistReturnedAppEncryptedTopicsMap(
736             long epochId, Map<Pair<String, String>, EncryptedTopic> returnedAppSdkEncryptedTopics) {
737         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
738         if (db == null) {
739             return;
740         }
741 
742         for (Map.Entry<Pair<String, String>, EncryptedTopic> app :
743                 returnedAppSdkEncryptedTopics.entrySet()) {
744             // Entry: Key = <Pair<App, Sdk>, Value = Topic.
745             ContentValues values = new ContentValues();
746             values.put(TopicsTables.ReturnedEncryptedTopicContract.EPOCH_ID, epochId);
747             values.put(TopicsTables.ReturnedEncryptedTopicContract.APP, app.getKey().first);
748             values.put(TopicsTables.ReturnedEncryptedTopicContract.SDK, app.getKey().second);
749             values.put(
750                     TopicsTables.ReturnedEncryptedTopicContract.ENCRYPTED_TOPIC,
751                     app.getValue().getEncryptedTopic());
752             values.put(
753                     TopicsTables.ReturnedEncryptedTopicContract.KEY_IDENTIFIER,
754                     app.getValue().getKeyIdentifier());
755             values.put(
756                     TopicsTables.ReturnedEncryptedTopicContract.ENCAPSULATED_KEY,
757                     app.getValue().getEncapsulatedKey());
758             try {
759                 db.insert(
760                         TopicsTables.ReturnedEncryptedTopicContract.TABLE,
761                         /* nullColumnHack */ null,
762                         values);
763             } catch (SQLException e) {
764                 sLogger.e(e, "Failed to record encrypted returned topic.");
765                 // TODO(b/307816452): Add client error log util here.
766             }
767         }
768     }
769 
770     /**
771      * Retrieve from the Topics ReturnedTopics Table and populate into the map. Will return topics
772      * for epoch with epochId in [epochId - numberOfLookBackEpochs + 1, epochId]
773      *
774      * @param epochId the current epochId
775      * @param numberOfLookBackEpochs How many epoch to look back. The current explainer uses 3
776      *     epochs
777      * @return a {@link Map} in type {@code Map<EpochId, Map < Pair < App, Sdk>, Topic>}
778      */
retrieveReturnedTopics( long epochId, int numberOfLookBackEpochs)779     public Map<Long, Map<Pair<String, String>, Topic>> retrieveReturnedTopics(
780             long epochId, int numberOfLookBackEpochs) {
781         Map<Long, Map<Pair<String, String>, Topic>> topicsMap = new HashMap<>();
782         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
783         if (db == null) {
784             return topicsMap;
785         }
786 
787         String[] projection = {
788             TopicsTables.ReturnedTopicContract.EPOCH_ID,
789             TopicsTables.ReturnedTopicContract.APP,
790             TopicsTables.ReturnedTopicContract.SDK,
791             TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION,
792             TopicsTables.ReturnedTopicContract.MODEL_VERSION,
793             TopicsTables.ReturnedTopicContract.TOPIC,
794         };
795         // Add LOGGED_TOPIC in the projection if flag ENABLE_LOGGED_TOPIC is true.
796         if (supportsLoggedTopicInReturnedTopicTable()) {
797             projection = new String[] {
798                     TopicsTables.ReturnedTopicContract.EPOCH_ID,
799                     TopicsTables.ReturnedTopicContract.APP,
800                     TopicsTables.ReturnedTopicContract.SDK,
801                     TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION,
802                     TopicsTables.ReturnedTopicContract.MODEL_VERSION,
803                     TopicsTables.ReturnedTopicContract.TOPIC,
804                     TopicsTables.ReturnedTopicContract.LOGGED_TOPIC,
805             };
806         }
807 
808         // Select epochId between [epochId - numberOfLookBackEpochs + 1, epochId]
809         String selection =
810                 " ? <= "
811                         + TopicsTables.ReturnedTopicContract.EPOCH_ID
812                         + " AND "
813                         + TopicsTables.ReturnedTopicContract.EPOCH_ID
814                         + " <= ?";
815         String[] selectionArgs = {
816             String.valueOf(epochId - numberOfLookBackEpochs + 1), String.valueOf(epochId)
817         };
818 
819         try (Cursor cursor =
820                 db.query(
821                         TopicsTables.ReturnedTopicContract.TABLE, // The table to query
822                         projection, // The array of columns to return (pass null to get all)
823                         selection, // The columns for the WHERE clause
824                         selectionArgs, // The values for the WHERE clause
825                         null, // don't group the rows
826                         null, // don't filter by row groups
827                         null // The sort order
828                         )) {
829             if (cursor == null) {
830                 return topicsMap;
831             }
832 
833             while (cursor.moveToNext()) {
834                 long cursorEpochId =
835                         cursor.getLong(
836                                 cursor.getColumnIndexOrThrow(
837                                         TopicsTables.ReturnedTopicContract.EPOCH_ID));
838                 String app =
839                         cursor.getString(
840                                 cursor.getColumnIndexOrThrow(
841                                         TopicsTables.ReturnedTopicContract.APP));
842                 String sdk =
843                         cursor.getString(
844                                 cursor.getColumnIndexOrThrow(
845                                         TopicsTables.ReturnedTopicContract.SDK));
846                 long taxonomyVersion =
847                         cursor.getLong(
848                                 cursor.getColumnIndexOrThrow(
849                                         TopicsTables.ReturnedTopicContract.TAXONOMY_VERSION));
850                 long modelVersion =
851                         cursor.getLong(
852                                 cursor.getColumnIndexOrThrow(
853                                         TopicsTables.ReturnedTopicContract.MODEL_VERSION));
854                 int topicId =
855                         cursor.getInt(
856                                 cursor.getColumnIndexOrThrow(
857                                         TopicsTables.ReturnedTopicContract.TOPIC));
858 
859                 // Building Map<EpochId, Map<Pair<AppId, AdTechId>, Topic>
860                 if (!topicsMap.containsKey(cursorEpochId)) {
861                     topicsMap.put(cursorEpochId, new HashMap<>());
862                 }
863 
864                 Topic topic = Topic.create(topicId, taxonomyVersion, modelVersion);
865                 // Add logged topic id to topic if flag ENABLE_LOGGED_TOPIC is true.
866                 if (supportsLoggedTopicInReturnedTopicTable()) {
867                     int loggedTopicId =
868                             cursor.getInt(
869                                     cursor.getColumnIndexOrThrow(
870                                             TopicsTables.ReturnedTopicContract.LOGGED_TOPIC));
871                     topic = Topic.create(
872                             topicId, taxonomyVersion, modelVersion, loggedTopicId);
873                 }
874 
875                 topicsMap.get(cursorEpochId).put(Pair.create(app, sdk), topic);
876             }
877         }
878 
879         return topicsMap;
880     }
881 
882     /**
883      * Retrieve from the ReturnedEncryptedTopic Table and populate into the map. Will return
884      * encrypted topics for epoch with epochId in [epochId - numberOfLookBackEpochs + 1, epochId]
885      *
886      * @param epochId the current epochId
887      * @param numberOfLookBackEpochs How many epoch to look back. The current explainer uses 3
888      *     epochs
889      * @return a {@link Map} in type {@code Map<EpochId, Map < Pair < App, Sdk>, EncryptedTopic>}
890      */
retrieveReturnedEncryptedTopics( long epochId, int numberOfLookBackEpochs)891     public Map<Long, Map<Pair<String, String>, EncryptedTopic>> retrieveReturnedEncryptedTopics(
892             long epochId, int numberOfLookBackEpochs) {
893         Map<Long, Map<Pair<String, String>, EncryptedTopic>> encryptedTopicsMap = new HashMap<>();
894         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
895         if (db == null) {
896             return encryptedTopicsMap;
897         }
898 
899         String[] projection = {
900             TopicsTables.ReturnedEncryptedTopicContract.EPOCH_ID,
901             TopicsTables.ReturnedEncryptedTopicContract.APP,
902             TopicsTables.ReturnedEncryptedTopicContract.SDK,
903             TopicsTables.ReturnedEncryptedTopicContract.ENCRYPTED_TOPIC,
904             TopicsTables.ReturnedEncryptedTopicContract.KEY_IDENTIFIER,
905             TopicsTables.ReturnedEncryptedTopicContract.ENCAPSULATED_KEY,
906         };
907 
908         // Select epochId between [epochId - numberOfLookBackEpochs + 1, epochId]
909         String selection =
910                 " ? <= "
911                         + TopicsTables.ReturnedEncryptedTopicContract.EPOCH_ID
912                         + " AND "
913                         + TopicsTables.ReturnedEncryptedTopicContract.EPOCH_ID
914                         + " <= ?";
915         String[] selectionArgs = {
916             String.valueOf(epochId - numberOfLookBackEpochs + 1), String.valueOf(epochId)
917         };
918 
919         try (Cursor cursor =
920                 db.query(
921                         TopicsTables.ReturnedEncryptedTopicContract.TABLE, // The table to query
922                         projection, // The array of columns to return (pass null to get all)
923                         selection, // The columns for the WHERE clause
924                         selectionArgs, // The values for the WHERE clause
925                         null, // don't group the rows
926                         null, // don't filter by row groups
927                         null // The sort order
928                         )) {
929             if (cursor == null) {
930                 return encryptedTopicsMap;
931             }
932 
933             while (cursor.moveToNext()) {
934                 long cursorEpochId =
935                         cursor.getLong(
936                                 cursor.getColumnIndexOrThrow(
937                                         TopicsTables.ReturnedEncryptedTopicContract.EPOCH_ID));
938                 String app =
939                         cursor.getString(
940                                 cursor.getColumnIndexOrThrow(
941                                         TopicsTables.ReturnedEncryptedTopicContract.APP));
942                 String sdk =
943                         cursor.getString(
944                                 cursor.getColumnIndexOrThrow(
945                                         TopicsTables.ReturnedEncryptedTopicContract.SDK));
946                 byte[] encryptedTopic =
947                         cursor.getBlob(
948                                 cursor.getColumnIndexOrThrow(
949                                         TopicsTables.ReturnedEncryptedTopicContract
950                                                 .ENCRYPTED_TOPIC));
951                 String keyIdentifier =
952                         cursor.getString(
953                                 cursor.getColumnIndexOrThrow(
954                                         TopicsTables.ReturnedEncryptedTopicContract
955                                                 .KEY_IDENTIFIER));
956                 byte[] encapsulatedKey =
957                         cursor.getBlob(
958                                 cursor.getColumnIndexOrThrow(
959                                         TopicsTables.ReturnedEncryptedTopicContract
960                                                 .ENCAPSULATED_KEY));
961 
962                 // Building Map<EpochId, Map<Pair<AppId, AdTechId>, Topic>
963                 if (!encryptedTopicsMap.containsKey(cursorEpochId)) {
964                     encryptedTopicsMap.put(cursorEpochId, new HashMap<>());
965                 }
966 
967                 EncryptedTopic topic =
968                         EncryptedTopic.create(encryptedTopic, keyIdentifier, encapsulatedKey);
969 
970                 encryptedTopicsMap.get(cursorEpochId).put(Pair.create(app, sdk), topic);
971             }
972         }
973 
974         return encryptedTopicsMap;
975     }
976 
977     /**
978      * Record {@link Topic} which should be blocked.
979      *
980      * @param topic {@link Topic} to block.
981      */
recordBlockedTopic(Topic topic)982     public void recordBlockedTopic(Topic topic) {
983         Objects.requireNonNull(topic);
984         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
985         if (db == null) {
986             return;
987         }
988         // Create a new map of values, where column names are the keys
989         ContentValues values = getContentValuesForBlockedTopic(topic);
990 
991         try {
992             db.insert(TopicsTables.BlockedTopicsContract.TABLE, /* nullColumnHack */ null, values);
993         } catch (SQLException e) {
994             sLogger.e("Failed to record blocked topic." + e.getMessage());
995             ErrorLogUtil.e(
996                     e,
997                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_RECORD_BLOCKED_TOPICS_FAILURE,
998                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
999         }
1000     }
1001 
getContentValuesForBlockedTopic(Topic topic)1002     private ContentValues getContentValuesForBlockedTopic(Topic topic) {
1003         // Create a new map of values, where column names are the keys
1004         ContentValues values = new ContentValues();
1005         values.put(TopicsTables.BlockedTopicsContract.TOPIC, topic.getTopic());
1006         values.put(TopicsTables.BlockedTopicsContract.TAXONOMY_VERSION, topic.getTaxonomyVersion());
1007         values.put(TopicsTables.BlockedTopicsContract.MODEL_VERSION, topic.getModelVersion());
1008         return values;
1009     }
1010 
1011     /**
1012      * Remove blocked {@link Topic}.
1013      *
1014      * @param topic blocked {@link Topic} to remove.
1015      */
removeBlockedTopic(Topic topic)1016     public void removeBlockedTopic(Topic topic) {
1017         Objects.requireNonNull(topic);
1018         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1019         if (db == null) {
1020             return;
1021         }
1022 
1023         // Where statement for triplet: topics, taxonomyVersion, modelVersion
1024         String whereClause =
1025                 " ? = "
1026                         + TopicsTables.BlockedTopicsContract.TOPIC
1027                         + " AND "
1028                         + TopicsTables.BlockedTopicsContract.TAXONOMY_VERSION
1029                         + " = ?"
1030                         + " AND "
1031                         + TopicsTables.BlockedTopicsContract.MODEL_VERSION
1032                         + " = ?";
1033         String[] whereArgs = {
1034             String.valueOf(topic.getTopic()),
1035             String.valueOf(topic.getTaxonomyVersion()),
1036             String.valueOf(topic.getModelVersion())
1037         };
1038 
1039         try {
1040             db.delete(TopicsTables.BlockedTopicsContract.TABLE, whereClause, whereArgs);
1041         } catch (SQLException e) {
1042             sLogger.e("Failed to delete blocked topic." + e.getMessage());
1043             ErrorLogUtil.e(
1044                     e,
1045                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_BLOCKED_TOPICS_FAILURE,
1046                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1047         }
1048     }
1049 
1050     /**
1051      * Get a {@link List} of {@link Topic}s which are blocked.
1052      *
1053      * @return {@link List} a {@link List} of blocked {@link Topic}s.s
1054      */
retrieveAllBlockedTopics()1055     public List<Topic> retrieveAllBlockedTopics() {
1056         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
1057         List<Topic> blockedTopics = new ArrayList<>();
1058         if (db == null) {
1059             return blockedTopics;
1060         }
1061 
1062         try (Cursor cursor =
1063                 db.query(
1064                         /* distinct= */ true,
1065                         TopicsTables.BlockedTopicsContract.TABLE, // The table to query
1066                         null, // Get all columns (null for all)
1067                         null, // Select all columns (null for all)
1068                         null, // Select all columns (null for all)
1069                         null, // Don't group the rows
1070                         null, // Don't filter by row groups
1071                         null, // don't sort
1072                         null // don't limit
1073                         )) {
1074             while (cursor.moveToNext()) {
1075                 long taxonomyVersion =
1076                         cursor.getLong(
1077                                 cursor.getColumnIndexOrThrow(
1078                                         TopicsTables.BlockedTopicsContract.TAXONOMY_VERSION));
1079                 long modelVersion =
1080                         cursor.getLong(
1081                                 cursor.getColumnIndexOrThrow(
1082                                         TopicsTables.BlockedTopicsContract.MODEL_VERSION));
1083                 int topicInt =
1084                         cursor.getInt(
1085                                 cursor.getColumnIndexOrThrow(
1086                                         TopicsTables.BlockedTopicsContract.TOPIC));
1087                 Topic topic = Topic.create(topicInt, taxonomyVersion, modelVersion);
1088 
1089                 blockedTopics.add(topic);
1090             }
1091         }
1092 
1093         return blockedTopics;
1094     }
1095 
1096     /**
1097      * Delete from epoch-related tables for data older than/equal to certain epoch in DB.
1098      *
1099      * @param tableName the table to delete data from
1100      * @param epochColumnName epoch Column name for given table
1101      * @param epochToDeleteFrom the epoch to delete starting from (inclusive)
1102      */
deleteDataOfOldEpochs( String tableName, String epochColumnName, long epochToDeleteFrom)1103     public void deleteDataOfOldEpochs(
1104             String tableName, String epochColumnName, long epochToDeleteFrom) {
1105         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1106         if (db == null) {
1107             return;
1108         }
1109 
1110         // Delete epochId before epochToDeleteFrom (including epochToDeleteFrom)
1111         String deletion = " " + epochColumnName + " <= ?";
1112         String[] deletionArgs = {String.valueOf(epochToDeleteFrom)};
1113 
1114         try {
1115             db.delete(tableName, deletion, deletionArgs);
1116         } catch (SQLException e) {
1117             sLogger.e(e, "Failed to delete old epochs' data.");
1118             ErrorLogUtil.e(
1119                     e,
1120                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_OLD_EPOCH_FAILURE,
1121                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1122         }
1123     }
1124 
1125     /**
1126      * Delete all data generated by Topics API, except for tables in the exclusion list.
1127      *
1128      * @param tablesToExclude a {@link List} of tables that won't be deleted.
1129      */
deleteAllTopicsTables(List<String> tablesToExclude)1130     public void deleteAllTopicsTables(List<String> tablesToExclude) {
1131         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1132         if (db == null) {
1133             return;
1134         }
1135 
1136         // Handle this in a transaction.
1137         db.beginTransaction();
1138 
1139         try {
1140             for (String table : ALL_TOPICS_TABLES) {
1141                 if (!tablesToExclude.contains(table)) {
1142                     try {
1143                         db.delete(table, /* whereClause= */ null, /* whereArgs= */ null);
1144                     } catch (SQLException e) {
1145                         sLogger.e(
1146                                 "Failed to delete %s table for Topics. Error: %s",
1147                                 table, e.getMessage());
1148                         ErrorLogUtil.e(
1149                                 e,
1150                                 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_TABLE_FAILURE,
1151                                 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1152                     }
1153                 }
1154             }
1155 
1156             // Mark the transaction successful.
1157             db.setTransactionSuccessful();
1158         } finally {
1159             db.endTransaction();
1160         }
1161     }
1162 
1163     /**
1164      * Delete by column for the given values. Allow passing in multiple tables with their
1165      * corresponding column names to delete by.
1166      *
1167      * @param tableNamesAndColumnNamePairs the tables and corresponding column names to remove
1168      *     entries from
1169      * @param valuesToDelete a {@link List} of values to delete if the entry has such value in
1170      *     {@code columnNameToDeleteFrom}
1171      */
deleteFromTableByColumn( List<Pair<String, String>> tableNamesAndColumnNamePairs, List<String> valuesToDelete)1172     public void deleteFromTableByColumn(
1173             List<Pair<String, String>> tableNamesAndColumnNamePairs, List<String> valuesToDelete) {
1174         Objects.requireNonNull(tableNamesAndColumnNamePairs);
1175         Objects.requireNonNull(valuesToDelete);
1176 
1177         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1178         // If valuesToDelete is empty, do nothing.
1179         if (db == null || valuesToDelete.isEmpty()) {
1180             return;
1181         }
1182 
1183         for (Pair<String, String> tableAndColumnNamePair : tableNamesAndColumnNamePairs) {
1184             String tableName = tableAndColumnNamePair.first;
1185             String columnNameToDeleteFrom = tableAndColumnNamePair.second;
1186 
1187             // Construct the "IN" part of SQL Query
1188             StringBuilder whereClauseBuilder = new StringBuilder();
1189             whereClauseBuilder.append("(?");
1190             for (int i = 0; i < valuesToDelete.size() - 1; i++) {
1191                 whereClauseBuilder.append(",?");
1192             }
1193             whereClauseBuilder.append(')');
1194 
1195             String whereClause = columnNameToDeleteFrom + " IN " + whereClauseBuilder;
1196             String[] whereArgs = valuesToDelete.toArray(new String[0]);
1197 
1198             try {
1199                 db.delete(tableName, whereClause, whereArgs);
1200             } catch (SQLException e) {
1201                 sLogger.e(
1202                         e,
1203                         String.format(
1204                                 "Failed to delete %s in table %s.", valuesToDelete, tableName));
1205                 ErrorLogUtil.e(
1206                         e,
1207                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_COLUMN_FAILURE,
1208                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1209             }
1210         }
1211     }
1212 
1213     /**
1214      * Delete an entry from tables if the value in the column of this entry exists in the given
1215      * values.
1216      *
1217      * <p>Similar to deleteEntriesFromTableByColumn but only delete entries that satisfy the equal
1218      * condition.
1219      *
1220      * @param tableNamesAndColumnNamePairs the tables and corresponding column names to remove
1221      *     entries from
1222      * @param valuesToDelete a {@link List} of values to delete if the entry has such value in
1223      *     {@code columnNameToDeleteFrom}
1224      * @param equalConditionColumnName the column name of the equal condition
1225      * @param equalConditionColumnValue the value in {@code equalConditionColumnName} of the equal
1226      *     condition
1227      * @param isStringEqualConditionColumnValue whether the value of {@code
1228      *     equalConditionColumnValue} is a string
1229      */
deleteEntriesFromTableByColumnWithEqualCondition( List<Pair<String, String>> tableNamesAndColumnNamePairs, List<String> valuesToDelete, String equalConditionColumnName, String equalConditionColumnValue, boolean isStringEqualConditionColumnValue)1230     public void deleteEntriesFromTableByColumnWithEqualCondition(
1231             List<Pair<String, String>> tableNamesAndColumnNamePairs,
1232             List<String> valuesToDelete,
1233             String equalConditionColumnName,
1234             String equalConditionColumnValue,
1235             boolean isStringEqualConditionColumnValue) {
1236         Objects.requireNonNull(tableNamesAndColumnNamePairs);
1237         Objects.requireNonNull(valuesToDelete);
1238         Objects.requireNonNull(equalConditionColumnName);
1239         Objects.requireNonNull(equalConditionColumnValue);
1240 
1241         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1242         // If valuesToDelete is empty, do nothing.
1243         if (db == null || valuesToDelete.isEmpty()) {
1244             return;
1245         }
1246 
1247         for (Pair<String, String> tableAndColumnNamePair : tableNamesAndColumnNamePairs) {
1248             String tableName = tableAndColumnNamePair.first;
1249             String columnNameToDeleteFrom = tableAndColumnNamePair.second;
1250 
1251             // Construct the "IN" part of SQL Query
1252             StringBuilder whereClauseBuilder = new StringBuilder();
1253             whereClauseBuilder.append("(?");
1254             for (int i = 0; i < valuesToDelete.size() - 1; i++) {
1255                 whereClauseBuilder.append(",?");
1256             }
1257             whereClauseBuilder.append(')');
1258 
1259             // Add equal condition to sql query. If the value is a string, bound it with single
1260             // quotes.
1261             String whereClause =
1262                     columnNameToDeleteFrom
1263                             + " IN "
1264                             + whereClauseBuilder
1265                             + " AND "
1266                             + equalConditionColumnName
1267                             + " = ";
1268             if (isStringEqualConditionColumnValue) {
1269                 whereClause += "'" + equalConditionColumnValue + "'";
1270             } else {
1271                 whereClause += equalConditionColumnValue;
1272             }
1273 
1274             try {
1275                 db.delete(tableName, whereClause, valuesToDelete.toArray(new String[0]));
1276             } catch (SQLException e) {
1277                 sLogger.e(
1278                         e,
1279                         String.format(
1280                                 "Failed to delete %s in table %s.", valuesToDelete, tableName));
1281                 ErrorLogUtil.e(
1282                         e,
1283                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_DELETE_COLUMN_FAILURE,
1284                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1285             }
1286         }
1287     }
1288 
1289     /**
1290      * Persist the origin's timestamp of epoch service in milliseconds into database.
1291      *
1292      * @param originTimestampMs the timestamp user first calls Topics API
1293      */
persistEpochOrigin(long originTimestampMs)1294     public void persistEpochOrigin(long originTimestampMs) {
1295         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1296         if (db == null) {
1297             return;
1298         }
1299 
1300         ContentValues values = new ContentValues();
1301         values.put(TopicsTables.EpochOriginContract.ORIGIN, originTimestampMs);
1302 
1303         try {
1304             db.insert(TopicsTables.EpochOriginContract.TABLE, /* nullColumnHack */ null, values);
1305         } catch (SQLException e) {
1306             sLogger.e("Failed to persist epoch origin." + e.getMessage());
1307         }
1308     }
1309 
1310     /**
1311      * Retrieve origin's timestamp of epoch service in milliseconds. If there is no origin persisted
1312      * in database, return -1;
1313      *
1314      * @return the origin's timestamp of epoch service in milliseconds. Return -1 if no origin is
1315      *     persisted.
1316      */
retrieveEpochOrigin()1317     public long retrieveEpochOrigin() {
1318         long origin = -1L;
1319 
1320         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
1321         if (db == null) {
1322             return origin;
1323         }
1324 
1325         String[] projection = {
1326             TopicsTables.EpochOriginContract.ORIGIN,
1327         };
1328 
1329         try (Cursor cursor =
1330                 db.query(
1331                         TopicsTables.EpochOriginContract.TABLE, // The table to query
1332                         projection, // The array of columns to return (pass null to get all)
1333                         null, // The columns for the WHERE clause
1334                         null, // The values for the WHERE clause
1335                         null, // don't group the rows
1336                         null, // don't filter by row groups
1337                         null // The sort order
1338                         )) {
1339             // Return the only entry in this table if existed.
1340             if (cursor.moveToNext()) {
1341                 origin =
1342                         cursor.getLong(
1343                                 cursor.getColumnIndexOrThrow(
1344                                         TopicsTables.EpochOriginContract.ORIGIN));
1345             }
1346         }
1347 
1348         return origin;
1349     }
1350 
1351     /**
1352      * Persist topic to contributor mappings to the database. In an epoch, an app is a contributor
1353      * to a topic if the app has called Topics API in this epoch and is classified to the topic.
1354      *
1355      * @param epochId the epochId
1356      * @param topicToContributorsMap a {@link Map} of topic to a @{@link Set} of its contributor
1357      *     apps.
1358      */
persistTopicContributors( long epochId, Map<Integer, Set<String>> topicToContributorsMap)1359     public void persistTopicContributors(
1360             long epochId, Map<Integer, Set<String>> topicToContributorsMap) {
1361         Objects.requireNonNull(topicToContributorsMap);
1362 
1363         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1364         if (db == null) {
1365             return;
1366         }
1367 
1368         for (Map.Entry<Integer, Set<String>> topicToContributors :
1369                 topicToContributorsMap.entrySet()) {
1370             Integer topicId = topicToContributors.getKey();
1371 
1372             for (String app : topicToContributors.getValue()) {
1373                 ContentValues values = new ContentValues();
1374                 values.put(TopicsTables.TopicContributorsContract.EPOCH_ID, epochId);
1375                 values.put(TopicsTables.TopicContributorsContract.TOPIC, topicId);
1376                 values.put(TopicsTables.TopicContributorsContract.APP, app);
1377 
1378                 try {
1379                     db.insert(
1380                             TopicsTables.TopicContributorsContract.TABLE,
1381                             /* nullColumnHack */ null,
1382                             values);
1383                 } catch (SQLException e) {
1384                     sLogger.e(e, "Failed to persist topic contributors.");
1385                     ErrorLogUtil.e(
1386                             e,
1387                             TOPICS_PERSIST_TOPICS_CONTRIBUTORS_FAILURE,
1388                             AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1389                 }
1390             }
1391         }
1392     }
1393 
1394     /**
1395      * Retrieve topic to contributor mappings from database. In an epoch, an app is a contributor to
1396      * a topic if the app has called Topics API in this epoch and is classified to the topic.
1397      *
1398      * @param epochId the epochId
1399      * @return a {@link Map} of topic to its contributors
1400      */
retrieveTopicToContributorsMap(long epochId)1401     public Map<Integer, Set<String>> retrieveTopicToContributorsMap(long epochId) {
1402         Map<Integer, Set<String>> topicToContributorsMap = new HashMap<>();
1403         SQLiteDatabase db = mDbHelper.safeGetReadableDatabase();
1404         if (db == null) {
1405             return topicToContributorsMap;
1406         }
1407 
1408         String[] projection = {
1409             TopicsTables.TopicContributorsContract.EPOCH_ID,
1410             TopicsTables.TopicContributorsContract.TOPIC,
1411             TopicsTables.TopicContributorsContract.APP
1412         };
1413 
1414         String selection = TopicsTables.TopicContributorsContract.EPOCH_ID + " = ?";
1415         String[] selectionArgs = {String.valueOf(epochId)};
1416 
1417         try (Cursor cursor =
1418                 db.query(
1419                         TopicsTables.TopicContributorsContract.TABLE, // The table to query
1420                         projection, // The array of columns to return (pass null to get all)
1421                         selection, // The columns for the WHERE clause
1422                         selectionArgs, // The values for the WHERE clause
1423                         null, // don't group the rows
1424                         null, // don't filter by row groups
1425                         null // The sort order
1426                         )) {
1427             if (cursor == null) {
1428                 return topicToContributorsMap;
1429             }
1430 
1431             while (cursor.moveToNext()) {
1432                 String app =
1433                         cursor.getString(
1434                                 cursor.getColumnIndexOrThrow(
1435                                         TopicsTables.TopicContributorsContract.APP));
1436                 int topicId =
1437                         cursor.getInt(
1438                                 cursor.getColumnIndexOrThrow(
1439                                         TopicsTables.TopicContributorsContract.TOPIC));
1440 
1441                 topicToContributorsMap.putIfAbsent(topicId, new HashSet<>());
1442                 topicToContributorsMap.get(topicId).add(app);
1443             }
1444         }
1445 
1446         return topicToContributorsMap;
1447     }
1448 
1449     /**
1450      * Delete all entries from a table.
1451      *
1452      * @param tableName the table to delete entries from
1453      */
deleteAllEntriesFromTable(String tableName)1454     public void deleteAllEntriesFromTable(String tableName) {
1455         SQLiteDatabase db = mDbHelper.safeGetWritableDatabase();
1456         if (db == null) {
1457             return;
1458         }
1459 
1460         try {
1461             db.delete(tableName, /* whereClause */ "", /* whereArgs */ new String[0]);
1462         } catch (SQLException e) {
1463             sLogger.e(
1464                     "Failed to delete all entries from table %s. Error: %s",
1465                     tableName, e.getMessage());
1466             ErrorLogUtil.e(
1467                     e,
1468                     TOPICS_DELETE_ALL_ENTRIES_IN_TABLE_FAILURE,
1469                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS);
1470         }
1471     }
1472 
1473     /** Check whether Logged Topic is supported in ReturnedTopic Table . */
supportsLoggedTopicInReturnedTopicTable()1474     public boolean supportsLoggedTopicInReturnedTopicTable() {
1475         return mDbHelper.supportsLoggedTopicInReturnedTopicTable();
1476     }
1477 }
1478