1 /* 2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.adservices.data.adselection; 18 19 import android.adservices.common.AdTechIdentifier; 20 import android.adservices.common.FrequencyCapFilters; 21 import android.content.pm.PackageManager; 22 23 import androidx.annotation.NonNull; 24 import androidx.annotation.Nullable; 25 import androidx.room.Dao; 26 import androidx.room.Insert; 27 import androidx.room.OnConflictStrategy; 28 import androidx.room.Query; 29 import androidx.room.Transaction; 30 31 import com.android.adservices.data.common.CleanupUtils; 32 import com.android.adservices.data.enrollment.EnrollmentDao; 33 import com.android.adservices.service.Flags; 34 import com.android.adservices.service.adselection.HistogramEvent; 35 36 import com.google.common.base.Preconditions; 37 38 import java.time.Instant; 39 import java.util.Arrays; 40 import java.util.List; 41 import java.util.Objects; 42 import java.util.Set; 43 44 /** 45 * DAO used to access ad counter histogram data used in frequency cap filtering during ad selection. 46 * 47 * <p>Annotated abstract methods will generate their own Room DB implementations. 48 */ 49 @Dao 50 public abstract class FrequencyCapDao { 51 /** 52 * Attempts to persist a new {@link DBHistogramIdentifier} to the identifier table. 53 * 54 * <p>If there is already an identifier persisted with the same foreign key (see {@link 55 * DBHistogramIdentifier#getHistogramIdentifierForeignKey()}), the transaction is canceled and 56 * rolled back. 57 * 58 * <p>This method is not intended to be called on its own. Please use {@link 59 * #insertHistogramEvent(HistogramEvent, int, int, int, int)} instead. 60 * 61 * @return the row ID of the identifier in the table, or {@code -1} if the specified row ID is 62 * already occupied 63 */ 64 @Insert(onConflict = OnConflictStrategy.ABORT) insertNewHistogramIdentifier(@onNull DBHistogramIdentifier identifier)65 protected abstract long insertNewHistogramIdentifier(@NonNull DBHistogramIdentifier identifier); 66 67 /** 68 * Returns the foreign key ID for the identifier matching the given constraints, or {@code null} 69 * if no match is found. 70 * 71 * <p>If multiple matches are found, only the first (as ordered by the numerical ID) is 72 * returned. 73 * 74 * <p>This method is not intended to be called on its own. It should only be used in {@link 75 * #insertHistogramEvent(HistogramEvent, int, int, int, int)}. 76 * 77 * @return the row ID of the identifier in the table, or {@code null} if not found 78 */ 79 @Query( 80 "SELECT foreign_key_id FROM fcap_histogram_ids " 81 + "WHERE buyer = :buyer " 82 // Note that the IS operator in SQLite specifically is equivalent to = for value 83 // matching except that it also matches NULL 84 + "AND custom_audience_owner IS :customAudienceOwner " 85 + "AND custom_audience_name IS :customAudienceName " 86 + "AND source_app IS :sourceApp " 87 + "ORDER BY foreign_key_id ASC " 88 + "LIMIT 1") 89 @Nullable getHistogramIdentifierForeignKeyIfExists( @onNull AdTechIdentifier buyer, @Nullable String customAudienceOwner, @Nullable String customAudienceName, @Nullable String sourceApp)90 protected abstract Long getHistogramIdentifierForeignKeyIfExists( 91 @NonNull AdTechIdentifier buyer, 92 @Nullable String customAudienceOwner, 93 @Nullable String customAudienceName, 94 @Nullable String sourceApp); 95 96 /** 97 * Attempts to persist a new {@link DBHistogramEventData} to the event data table. 98 * 99 * <p>If there is already an entry in the table with the same non-{@code null} row ID, the 100 * transaction is canceled and rolled back. 101 * 102 * <p>This method is not intended to be called on its own. Please use {@link 103 * #insertHistogramEvent(HistogramEvent, int, int, int, int)} instead. 104 * 105 * @return the row ID of the event data in the table, or -1 if the event data already exists 106 */ 107 @Insert(onConflict = OnConflictStrategy.ABORT) insertNewHistogramEventData(@onNull DBHistogramEventData eventData)108 protected abstract long insertNewHistogramEventData(@NonNull DBHistogramEventData eventData); 109 110 /** 111 * Attempts to insert a {@link HistogramEvent} into the histogram tables in a single 112 * transaction. 113 * 114 * <p>If the current number of events in the histogram table is larger than the given {@code 115 * absoluteMaxTotalHistogramEventCount}, then the oldest events in the table will be evicted so 116 * that the count of events is the given {@code lowerMaxTotalHistogramEventCount}. 117 * 118 * @throws IllegalStateException if an error was encountered adding the event 119 */ 120 @Transaction insertHistogramEvent( @onNull HistogramEvent event, int absoluteMaxTotalHistogramEventCount, int lowerMaxTotalHistogramEventCount, int absoluteMaxPerBuyerHistogramEventCount, int lowerMaxPerBuyerHistogramEventCount)121 public void insertHistogramEvent( 122 @NonNull HistogramEvent event, 123 int absoluteMaxTotalHistogramEventCount, 124 int lowerMaxTotalHistogramEventCount, 125 int absoluteMaxPerBuyerHistogramEventCount, 126 int lowerMaxPerBuyerHistogramEventCount) 127 throws IllegalStateException { 128 Objects.requireNonNull(event); 129 Preconditions.checkArgument(absoluteMaxTotalHistogramEventCount > 0); 130 Preconditions.checkArgument(lowerMaxTotalHistogramEventCount > 0); 131 Preconditions.checkArgument(absoluteMaxPerBuyerHistogramEventCount > 0); 132 Preconditions.checkArgument(lowerMaxPerBuyerHistogramEventCount > 0); 133 Preconditions.checkArgument( 134 absoluteMaxTotalHistogramEventCount > lowerMaxTotalHistogramEventCount); 135 Preconditions.checkArgument( 136 absoluteMaxPerBuyerHistogramEventCount > lowerMaxPerBuyerHistogramEventCount); 137 138 // TODO(b/275581841): Collect and send telemetry on frequency cap eviction 139 int numEventsDeleted = 0; 140 141 // Check the table size first and evict older events if necessary 142 int currentTotalHistogramEventCount = getTotalNumHistogramEvents(); 143 if (currentTotalHistogramEventCount >= absoluteMaxTotalHistogramEventCount) { 144 int numEventsToDelete = 145 currentTotalHistogramEventCount - lowerMaxTotalHistogramEventCount; 146 numEventsDeleted += deleteOldestHistogramEventData(numEventsToDelete); 147 } 148 149 // Check the per-buyer quota 150 int currentPerBuyerHistogramEventCount = getNumHistogramEventsByBuyer(event.getBuyer()); 151 if (currentPerBuyerHistogramEventCount >= absoluteMaxPerBuyerHistogramEventCount) { 152 int numEventsToDelete = 153 currentPerBuyerHistogramEventCount - lowerMaxPerBuyerHistogramEventCount; 154 numEventsDeleted += 155 deleteOldestHistogramEventDataByBuyer(event.getBuyer(), numEventsToDelete); 156 } 157 158 // Be efficient with I/O operations; background maintenance job will also clean up data 159 if (numEventsDeleted > 0) { 160 deleteUnpairedHistogramIdentifiers(); 161 } 162 163 // Converting to DBHistogramIdentifier drops custom audience fields if the type is WIN 164 DBHistogramIdentifier identifier = DBHistogramIdentifier.fromHistogramEvent(event); 165 Long foreignKeyId = 166 getHistogramIdentifierForeignKeyIfExists( 167 identifier.getBuyer(), 168 identifier.getCustomAudienceOwner(), 169 identifier.getCustomAudienceName(), 170 identifier.getSourceApp()); 171 172 if (foreignKeyId == null) { 173 try { 174 foreignKeyId = insertNewHistogramIdentifier(identifier); 175 } catch (Exception exception) { 176 throw new IllegalStateException("Error inserting histogram identifier", exception); 177 } 178 } 179 180 try { 181 insertNewHistogramEventData( 182 DBHistogramEventData.fromHistogramEvent(foreignKeyId, event)); 183 } catch (Exception exception) { 184 throw new IllegalStateException("Error inserting histogram event data", exception); 185 } 186 } 187 188 /** 189 * Returns the number of events in the appropriate buyer's histograms that have been registered 190 * since the given timestamp. 191 * 192 * @return the number of found events that match the criteria 193 */ 194 @Query( 195 "SELECT COUNT(DISTINCT data.row_id) FROM fcap_histogram_data AS data " 196 + "INNER JOIN fcap_histogram_ids AS ids " 197 + "ON data.foreign_key_id = ids.foreign_key_id " 198 + "WHERE data.ad_counter_int_key = :adCounterIntKey " 199 + "AND ids.buyer = :buyer " 200 + "AND data.ad_event_type = :adEventType " 201 + "AND data.timestamp >= :startTime") getNumEventsForBuyerAfterTime( int adCounterIntKey, @NonNull AdTechIdentifier buyer, @FrequencyCapFilters.AdEventType int adEventType, @NonNull Instant startTime)202 public abstract int getNumEventsForBuyerAfterTime( 203 int adCounterIntKey, 204 @NonNull AdTechIdentifier buyer, 205 @FrequencyCapFilters.AdEventType int adEventType, 206 @NonNull Instant startTime); 207 208 /** 209 * Returns the number of events in the appropriate custom audience's histogram that have been 210 * registered since the given timestamp. 211 * 212 * @return the number of found events that match the criteria 213 */ 214 @Query( 215 "SELECT COUNT(DISTINCT data.row_id) FROM fcap_histogram_data AS data " 216 + "INNER JOIN fcap_histogram_ids AS ids " 217 + "ON data.foreign_key_id = ids.foreign_key_id " 218 + "WHERE data.ad_counter_int_key = :adCounterIntKey " 219 + "AND ids.buyer = :buyer " 220 + "AND ids.custom_audience_owner = :customAudienceOwner " 221 + "AND ids.custom_audience_name = :customAudienceName " 222 + "AND data.ad_event_type = :adEventType " 223 + "AND data.timestamp >= :startTime") getNumEventsForCustomAudienceAfterTime( int adCounterIntKey, @NonNull AdTechIdentifier buyer, @NonNull String customAudienceOwner, @NonNull String customAudienceName, @FrequencyCapFilters.AdEventType int adEventType, @NonNull Instant startTime)224 public abstract int getNumEventsForCustomAudienceAfterTime( 225 int adCounterIntKey, 226 @NonNull AdTechIdentifier buyer, 227 @NonNull String customAudienceOwner, 228 @NonNull String customAudienceName, 229 @FrequencyCapFilters.AdEventType int adEventType, 230 @NonNull Instant startTime); 231 232 /** 233 * Deletes all histogram event data older than the given {@code expiryTime}. 234 * 235 * <p>This method is not intended to be called on its own. Please use {@link 236 * #deleteAllExpiredHistogramData(Instant)} instead. 237 * 238 * @return the number of deleted events 239 */ 240 @Query("DELETE FROM fcap_histogram_data WHERE timestamp < :expiryTime") deleteHistogramEventDataBeforeTime(@onNull Instant expiryTime)241 protected abstract int deleteHistogramEventDataBeforeTime(@NonNull Instant expiryTime); 242 243 /** 244 * Deletes the oldest {@code N} histogram events, where {@code N} is at most {@code 245 * numEventsToDelete}, and returns the number of entries deleted. 246 * 247 * <p>This method is not meant to be called on its own. Please use {@link 248 * #insertHistogramEvent(HistogramEvent, int, int, int, int)} to evict data when the table is 249 * full. 250 */ 251 @Query( 252 "DELETE FROM fcap_histogram_data " 253 + "WHERE row_id IN " 254 + "(SELECT row_id FROM fcap_histogram_data " 255 + "ORDER BY timestamp ASC " 256 + "LIMIT :numEventsToDelete)") deleteOldestHistogramEventData(int numEventsToDelete)257 protected abstract int deleteOldestHistogramEventData(int numEventsToDelete); 258 259 /** 260 * Deletes the oldest {@code N} histogram events that belong to a given {@code buyer}, where 261 * {@code N} is at most {@code numEventsToDelete}, and returns the number of entries deleted. 262 * 263 * <p>This method is not meant to be called on its own. Please use {@link 264 * #insertHistogramEvent(HistogramEvent, int, int, int, int)} to evict data when the table is 265 * full. 266 */ 267 @Query( 268 "DELETE FROM fcap_histogram_data " 269 + "WHERE row_id IN " 270 + "(SELECT data.row_id FROM fcap_histogram_data AS data " 271 + "INNER JOIN fcap_histogram_ids AS ids " 272 + "ON data.foreign_key_id = ids.foreign_key_id " 273 + "WHERE ids.buyer = :buyer " 274 + "ORDER BY data.timestamp ASC " 275 + "LIMIT :numEventsToDelete)") deleteOldestHistogramEventDataByBuyer( @onNull AdTechIdentifier buyer, int numEventsToDelete)276 protected abstract int deleteOldestHistogramEventDataByBuyer( 277 @NonNull AdTechIdentifier buyer, int numEventsToDelete); 278 279 /** 280 * Deletes histogram identifiers which have no associated event data. 281 * 282 * <p>This method is not intended to be called on its own. Please use {@link 283 * #deleteAllExpiredHistogramData(Instant)} instead. 284 * 285 * @return the number of deleted identifiers 286 */ 287 @Query( 288 "DELETE FROM fcap_histogram_ids " 289 + "WHERE foreign_key_id NOT IN " 290 + "(SELECT ids.foreign_key_id FROM fcap_histogram_ids AS ids " 291 + "INNER JOIN fcap_histogram_data AS data " 292 + "ON ids.foreign_key_id = data.foreign_key_id)") deleteUnpairedHistogramIdentifiers()293 protected abstract int deleteUnpairedHistogramIdentifiers(); 294 295 /** 296 * Deletes all histogram data older than the given {@code expiryTime} in a single database 297 * transaction. 298 * 299 * <p>Also cleans up any histogram identifiers which are no longer associated with any event 300 * data. 301 * 302 * @return the number of deleted events 303 */ 304 @Transaction deleteAllExpiredHistogramData(@onNull Instant expiryTime)305 public int deleteAllExpiredHistogramData(@NonNull Instant expiryTime) { 306 Objects.requireNonNull(expiryTime); 307 308 int numDeletedEvents = deleteHistogramEventDataBeforeTime(expiryTime); 309 deleteUnpairedHistogramIdentifiers(); 310 return numDeletedEvents; 311 } 312 313 /** 314 * Deletes all histogram event data persisted by the given {@code sourceApp}. 315 * 316 * <p>This method is not intended to be called on its own. Please use {@link 317 * #deleteHistogramDataBySourceApp} instead. 318 * 319 * @return the number of deleted events 320 */ 321 @Query( 322 "DELETE FROM fcap_histogram_data " 323 + "WHERE row_id IN " 324 + "(SELECT data.row_id FROM fcap_histogram_data AS data " 325 + "INNER JOIN fcap_histogram_ids AS ids " 326 + "ON data.foreign_key_id = ids.foreign_key_id " 327 + "WHERE ids.source_app = :sourceApp)") deleteHistogramEventDataBySourceApp(@onNull String sourceApp)328 protected abstract int deleteHistogramEventDataBySourceApp(@NonNull String sourceApp); 329 330 /** 331 * Deletes all histogram event data persisted by the given {@code sourceApp} in a single 332 * database transaction. 333 * 334 * <p>Also cleans up any histogram identifiers which are no longer associated with any event 335 * data. 336 * 337 * @return the number of deleted events 338 */ 339 @Transaction deleteHistogramDataBySourceApp(@onNull String sourceApp)340 public int deleteHistogramDataBySourceApp(@NonNull String sourceApp) { 341 Objects.requireNonNull(sourceApp); 342 343 int numDeletedEvents = deleteHistogramEventDataBySourceApp(sourceApp); 344 deleteUnpairedHistogramIdentifiers(); 345 return numDeletedEvents; 346 } 347 348 /** 349 * Deletes all histogram event data. 350 * 351 * <p>This method is not meant to be called on its own. Please use {@link 352 * #deleteAllHistogramData()} to delete all histogram data (including all identifiers). 353 * 354 * @return the number of deleted events 355 */ 356 @Query("DELETE FROM fcap_histogram_data") deleteAllHistogramEventData()357 protected abstract int deleteAllHistogramEventData(); 358 359 /** 360 * Deletes all histogram identifiers. 361 * 362 * <p>This method is not meant to be called on its own. Please use {@link 363 * #deleteAllHistogramData()} to delete all histogram data (including all identifiers). 364 * 365 * @return the number of deleted identifiers 366 */ 367 @Query("DELETE FROM fcap_histogram_ids") deleteAllHistogramIdentifiers()368 protected abstract int deleteAllHistogramIdentifiers(); 369 370 /** 371 * Deletes all histogram data and identifiers in a single database transaction. 372 * 373 * @return the number of deleted events 374 */ 375 @Transaction deleteAllHistogramData()376 public int deleteAllHistogramData() { 377 int numDeletedEvents = deleteAllHistogramEventData(); 378 deleteAllHistogramIdentifiers(); 379 return numDeletedEvents; 380 } 381 382 /** Returns the list of all unique buyer ad techs in the histogram ID table. */ 383 @Query("SELECT DISTINCT buyer FROM fcap_histogram_ids") 384 @NonNull getAllHistogramBuyers()385 public abstract List<AdTechIdentifier> getAllHistogramBuyers(); 386 387 /** 388 * Deletes all histogram event data belonging to the given buyer ad techs. 389 * 390 * <p>This method is not meant to be called on its own. Please use {@link 391 * #deleteAllDisallowedBuyerHistogramData(EnrollmentDao)} to delete all histogram data 392 * (including all identifiers). 393 * 394 * @return the number of deleted histogram events 395 */ 396 @Query( 397 "DELETE FROM fcap_histogram_data WHERE foreign_key_id in (SELECT DISTINCT" 398 + " foreign_key_id FROM fcap_histogram_ids WHERE buyer in (:buyers))") deleteHistogramEventDataByBuyers(@onNull List<AdTechIdentifier> buyers)399 protected abstract int deleteHistogramEventDataByBuyers(@NonNull List<AdTechIdentifier> buyers); 400 401 /** 402 * Deletes all histogram data belonging to disallowed buyer ad techs in a single transaction, 403 * where the buyer ad techs cannot be found in the enrollment database. 404 * 405 * @return the number of deleted histogram events 406 */ 407 @Transaction deleteAllDisallowedBuyerHistogramData(@onNull EnrollmentDao enrollmentDao)408 public int deleteAllDisallowedBuyerHistogramData(@NonNull EnrollmentDao enrollmentDao) { 409 Objects.requireNonNull(enrollmentDao); 410 411 List<AdTechIdentifier> buyersToRemove = getAllHistogramBuyers(); 412 if (buyersToRemove.isEmpty()) { 413 return 0; 414 } 415 416 Set<AdTechIdentifier> allFledgeEnrolledAdTechs = 417 enrollmentDao.getAllFledgeEnrolledAdTechs(); 418 buyersToRemove.removeAll(allFledgeEnrolledAdTechs); 419 420 int numDeletedEvents = 0; 421 if (!buyersToRemove.isEmpty()) { 422 numDeletedEvents = deleteHistogramEventDataByBuyers(buyersToRemove); 423 // TODO(b/275581841): Collect and send telemetry on frequency cap deletion 424 deleteUnpairedHistogramIdentifiers(); 425 } 426 427 return numDeletedEvents; 428 } 429 430 /** Returns the list of all unique buyer ad techs in the histogram ID table. */ 431 @Query("SELECT DISTINCT source_app FROM fcap_histogram_ids") getAllHistogramSourceApps()432 public abstract List<String> getAllHistogramSourceApps(); 433 434 /** 435 * Deletes all histogram event data generated in the given source apps. 436 * 437 * <p>This method is not meant to be called on its own. Please use {@link 438 * #deleteAllDisallowedSourceAppHistogramData(PackageManager, Flags)} to delete all histogram 439 * data (including all identifiers). 440 * 441 * @return the number of deleted histogram events 442 */ 443 @Query( 444 "DELETE FROM fcap_histogram_data WHERE foreign_key_id in (SELECT DISTINCT" 445 + " foreign_key_id FROM fcap_histogram_ids WHERE source_app in (:sourceApps))") deleteHistogramEventDataBySourceApps(@onNull List<String> sourceApps)446 protected abstract int deleteHistogramEventDataBySourceApps(@NonNull List<String> sourceApps); 447 448 /** 449 * Deletes all histogram data belonging to disallowed source apps in a single transaction, where 450 * the source apps cannot be found in the app package name allowlist or are not installed on the 451 * device. 452 * 453 * @return the number of deleted histogram events 454 */ 455 @Transaction deleteAllDisallowedSourceAppHistogramData( @onNull PackageManager packageManager, @NonNull Flags flags)456 public int deleteAllDisallowedSourceAppHistogramData( 457 @NonNull PackageManager packageManager, @NonNull Flags flags) { 458 Objects.requireNonNull(packageManager); 459 Objects.requireNonNull(flags); 460 461 List<String> sourceAppsToRemove = getAllHistogramSourceApps(); 462 if (sourceAppsToRemove.isEmpty()) { 463 return 0; 464 } 465 466 CleanupUtils.removeAllowedPackages( 467 sourceAppsToRemove, 468 packageManager, 469 Arrays.asList(flags.getPpapiAppAllowList(), flags.getPasAppAllowList())); 470 471 int numDeletedEvents = 0; 472 if (!sourceAppsToRemove.isEmpty()) { 473 numDeletedEvents = deleteHistogramEventDataBySourceApps(sourceAppsToRemove); 474 // TODO(b/275581841): Collect and send telemetry on frequency cap deletion 475 deleteUnpairedHistogramIdentifiers(); 476 } 477 478 return numDeletedEvents; 479 } 480 481 /** Returns the current total number of histogram events in the data table. */ 482 @Query("SELECT COUNT(DISTINCT row_id) FROM fcap_histogram_data") getTotalNumHistogramEvents()483 public abstract int getTotalNumHistogramEvents(); 484 485 /** Returns the current total number of histogram identifiers in the identifier table. */ 486 @Query("SELECT COUNT(DISTINCT foreign_key_id) FROM fcap_histogram_ids") getTotalNumHistogramIdentifiers()487 public abstract int getTotalNumHistogramIdentifiers(); 488 489 /** 490 * Returns the current number of histogram events in the data table for the given {@code buyer}. 491 */ 492 @Query( 493 "SELECT COUNT(DISTINCT data.row_id) FROM fcap_histogram_data AS data " 494 + "INNER JOIN fcap_histogram_ids AS ids " 495 + "ON data.foreign_key_id = ids.foreign_key_id " 496 + "WHERE ids.buyer = :buyer ") getNumHistogramEventsByBuyer(@onNull AdTechIdentifier buyer)497 public abstract int getNumHistogramEventsByBuyer(@NonNull AdTechIdentifier buyer); 498 499 /** 500 * Returns the current number of histogram events in the data table for the given {@code 501 * sourceApp}. 502 */ 503 @Query( 504 "SELECT COUNT(DISTINCT data.row_id) FROM fcap_histogram_data AS data " 505 + "INNER JOIN fcap_histogram_ids AS ids " 506 + "ON data.foreign_key_id = ids.foreign_key_id " 507 + "WHERE ids.source_app = :sourceApp ") getNumHistogramEventsBySourceApp(@onNull String sourceApp)508 public abstract int getNumHistogramEventsBySourceApp(@NonNull String sourceApp); 509 } 510