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