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