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