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