• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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.service.measurement.registration;
18 
19 import android.annotation.NonNull;
20 import android.net.Uri;
21 import android.util.Pair;
22 
23 import com.android.adservices.LoggerFactory;
24 import com.android.adservices.data.measurement.DatastoreException;
25 import com.android.adservices.data.measurement.IMeasurementDao;
26 import com.android.adservices.service.Flags;
27 import com.android.adservices.service.FlagsFactory;
28 import com.android.adservices.service.common.WebAddresses;
29 import com.android.adservices.service.measurement.EventSurfaceType;
30 import com.android.adservices.service.measurement.Source;
31 import com.android.adservices.service.measurement.reporting.DebugReportApi;
32 import com.android.adservices.service.measurement.util.BaseUriExtractor;
33 
34 import java.util.List;
35 import java.util.Map;
36 import java.util.Optional;
37 import java.util.Set;
38 
39 /** Validation for inserting source to database */
40 public class SourceEligibilityChecker {
41     private final Flags mFlags;
42     private final DebugReportApi mDebugReportApi;
43 
SourceEligibilityChecker(Flags flags, DebugReportApi debugReportApi)44     public SourceEligibilityChecker(Flags flags, DebugReportApi debugReportApi) {
45         mFlags = flags;
46         mDebugReportApi = debugReportApi;
47     }
48 
49     enum InsertSourcePermission {
50         NOT_ALLOWED(false),
51         ALLOWED(true),
52         ALLOWED_FIFO_SUCCESS(true);
53 
54         private final boolean mIsAllowed;
55 
InsertSourcePermission(boolean isAllowed)56         InsertSourcePermission(boolean isAllowed) {
57             mIsAllowed = isAllowed;
58         }
59 
isAllowed()60         public boolean isAllowed() {
61             return mIsAllowed;
62         }
63     }
64 
65     /**
66      * Determines the permission for inserting a source into the database.
67      *
68      * @param source incoming {@link Source}
69      * @param topOrigin a {@link Uri}
70      * @param publisherType represents an event surface type
71      * @param dao a {@link IMeasurementDao}
72      * @param asyncFetchStatus a {@link AsyncFetchStatus}, stores Ad Tech server status
73      * @param adrTypes a set of types for aggregate debug reporting
74      * @return a {@link InsertSourcePermission}, indicating whether the source can be inserted
75      */
isAllowedToInsert( Source source, Uri topOrigin, @EventSurfaceType int publisherType, IMeasurementDao dao, AsyncFetchStatus asyncFetchStatus, Set<DebugReportApi.Type> adrTypes)76     public InsertSourcePermission isAllowedToInsert(
77             Source source,
78             Uri topOrigin,
79             @EventSurfaceType int publisherType,
80             IMeasurementDao dao,
81             AsyncFetchStatus asyncFetchStatus,
82             Set<DebugReportApi.Type> adrTypes)
83             throws DatastoreException {
84         // Do not persist the navigation source if the same reporting origin has been registered
85         // for the registration.
86         if (isNavigationOriginAlreadyRegisteredForRegistration(source, dao)) {
87             LoggerFactory.getMeasurementLogger()
88                     .d(
89                             "storeSource (FAILURE): Duplicate reporting origin in registration"
90                                 + " sequence. Enrollment ID: %s, Source ID: %s, Source Event ID:"
91                                 + " %s",
92                             source.getEnrollmentId(), source.getId(), source.getEventId());
93             return InsertSourcePermission.NOT_ALLOWED;
94         }
95         long windowStartTime =
96                 source.getEventTime() - mFlags.getMeasurementRateLimitWindowMilliseconds();
97         Optional<Uri> publisher = getTopLevelPublisher(topOrigin, publisherType);
98         if (publisher.isEmpty()) {
99             LoggerFactory.getMeasurementLogger()
100                     .d(
101                             "storeSource (FAILURE): getTopLevelPublisher failed. topOrigin: %s,"
102                                     + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s",
103                             topOrigin,
104                             source.getEnrollmentId(),
105                             source.getId(),
106                             source.getEventId());
107             return InsertSourcePermission.NOT_ALLOWED;
108         }
109         long numOfSourcesPerPublisher =
110                 dao.getNumSourcesPerPublisher(
111                         BaseUriExtractor.getBaseUri(topOrigin), publisherType);
112         if (numOfSourcesPerPublisher >= mFlags.getMeasurementMaxSourcesPerPublisher()) {
113             LoggerFactory.getMeasurementLogger()
114                     .d(
115                             "storeSource (FAILURE): Reached limit of %s sources for publisher - %s."
116                                     + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s",
117                             mFlags.getMeasurementMaxSourcesPerPublisher(),
118                             publisher,
119                             source.getEnrollmentId(),
120                             source.getId(),
121                             source.getEventId());
122             mDebugReportApi.scheduleSourceReport(
123                     source,
124                     DebugReportApi.Type.SOURCE_STORAGE_LIMIT,
125                     Map.of(DebugReportApi.Body.LIMIT, String.valueOf(numOfSourcesPerPublisher)),
126                     dao);
127             adrTypes.add(DebugReportApi.Type.SOURCE_STORAGE_LIMIT);
128             return InsertSourcePermission.NOT_ALLOWED;
129         }
130 
131         // Blocks ad-techs to register multiple sources with various destinations in a short window
132         // (per minute)
133         int destinationsPerMinuteRateLimit =
134                 mFlags.getMeasurementMaxDestPerPublisherXEnrollmentPerRateLimitWindow();
135         if (mFlags.getMeasurementEnableDestinationRateLimit()
136                 && sourceExceedsTimeBasedDestinationLimits(
137                         source,
138                         publisher.get(),
139                         publisherType,
140                         mFlags.getMeasurementDestinationRateLimitWindow(),
141                         destinationsPerMinuteRateLimit,
142                         dao)) {
143             LoggerFactory.getMeasurementLogger()
144                     .d(
145                             "storeSource (FAILURE): Exceeded limit of %s destinations per minute."
146                                     + " Enrollment ID: %s, Source ID: %s, Source Event ID: %s",
147                             destinationsPerMinuteRateLimit,
148                             source.getEnrollmentId(),
149                             source.getId(),
150                             source.getEventId());
151             mDebugReportApi.scheduleSourceDestinationPerMinuteRateLimitDebugReport(
152                     source, String.valueOf(destinationsPerMinuteRateLimit), dao);
153             adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_RATE_LIMIT);
154             return InsertSourcePermission.NOT_ALLOWED;
155         }
156 
157         // Global (cross reporting-origin) destinations rate limit. This needs to be recorded before
158         // FIFO based deletion as it's a LIFO based rate limit. Although reject the source if it
159         // fails only if every other (enrollment based) rate limit passes to not reveal cross site
160         // data.
161         boolean destinationExceedsGlobalRateLimit =
162                 destinationExceedsGlobalRateLimit(source, publisher.get(), dao);
163 
164         // Blocks ad-techs to reconstruct browser history by registering multiple sources with
165         // various destinations in a medium window (per day). The larger window is 30 days.
166         int destinationsPerDayRateLimit = mFlags.getMeasurementDestinationPerDayRateLimit();
167         if (mFlags.getMeasurementEnableDestinationPerDayRateLimitWindow()
168                 && sourceExceedsTimeBasedDestinationLimits(
169                         source,
170                         publisher.get(),
171                         publisherType,
172                         mFlags.getMeasurementDestinationPerDayRateLimitWindowInMs(),
173                         destinationsPerDayRateLimit,
174                         dao)) {
175             LoggerFactory.getMeasurementLogger()
176                     .d(
177                             "storeSource (FAILURE): Exceeded limit of %s destinations per"
178                                     + " day. Enrollment ID: %s, Source ID: %s, Source Event ID: %s",
179                             destinationsPerDayRateLimit,
180                             source.getEnrollmentId(),
181                             source.getId(),
182                             source.getEventId());
183             mDebugReportApi.scheduleSourceDestinationPerDayRateLimitDebugReport(
184                     source, String.valueOf(destinationsPerDayRateLimit), dao);
185             adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_PER_DAY_RATE_LIMIT);
186             return InsertSourcePermission.NOT_ALLOWED;
187         }
188 
189         if (source.getAppDestinations() != null
190                 && isDestinationOutOfBounds(
191                         source,
192                         publisher.get(),
193                         publisherType,
194                         source.getEnrollmentId(),
195                         source.getAppDestinations(),
196                         EventSurfaceType.APP,
197                         windowStartTime,
198                         source.getEventTime(),
199                         dao,
200                         adrTypes)) {
201             return InsertSourcePermission.NOT_ALLOWED;
202         }
203 
204         if (source.getWebDestinations() != null
205                 && isDestinationOutOfBounds(
206                         source,
207                         publisher.get(),
208                         publisherType,
209                         source.getEnrollmentId(),
210                         source.getWebDestinations(),
211                         EventSurfaceType.WEB,
212                         windowStartTime,
213                         source.getEventTime(),
214                         dao,
215                         adrTypes)) {
216             return InsertSourcePermission.NOT_ALLOWED;
217         }
218 
219         Map<String, Object> additionalDebugReportParams = null;
220         InsertSourcePermission result = InsertSourcePermission.ALLOWED;
221         // Should be deprecated once destination priority is fully launched
222         if (extractSourceDestinationLimitingAlgo(mFlags, source)
223                 == Source.DestinationLimitAlgorithm.FIFO) {
224             InsertSourcePermission appDestSourceAllowedToInsert =
225                     deleteLowPriorityDestinationSourcesToAccommodateNewSource(
226                             source,
227                             publisherType,
228                             dao,
229                             publisher.get(),
230                             EventSurfaceType.APP,
231                             source.getAppDestinations(),
232                             asyncFetchStatus);
233             if (appDestSourceAllowedToInsert == InsertSourcePermission.NOT_ALLOWED) {
234                 // Return early without checking web destinations
235                 LoggerFactory.getMeasurementLogger()
236                         .d(
237                                 "storeSource (FAILURE): Cannot make space for app destination in"
238                                         + " source. Enrollment ID: %s, Source ID: %s, Source"
239                                         + " Event ID: %s",
240                                 source.getEnrollmentId(), source.getId(), source.getEventId());
241                 mDebugReportApi.scheduleSourceDestinationLimitDebugReport(
242                         source,
243                         String.valueOf(
244                                 mFlags.getMeasurementMaxDistinctDestinationsInActiveSource()),
245                         dao);
246                 adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_LIMIT);
247                 return InsertSourcePermission.NOT_ALLOWED;
248             }
249             InsertSourcePermission webDestSourceAllowedToInsert =
250                     deleteLowPriorityDestinationSourcesToAccommodateNewSource(
251                             source,
252                             publisherType,
253                             dao,
254                             publisher.get(),
255                             EventSurfaceType.WEB,
256                             source.getWebDestinations(),
257                             asyncFetchStatus);
258             if (webDestSourceAllowedToInsert == InsertSourcePermission.NOT_ALLOWED) {
259                 LoggerFactory.getMeasurementLogger()
260                         .d(
261                                 "storeSource (FAILURE): Cannot make space for web destinations in"
262                                         + " source. Enrollment ID: %s, Source ID: %s, Source"
263                                         + " Event ID: %s",
264                                 source.getEnrollmentId(), source.getId(), source.getEventId());
265                 mDebugReportApi.scheduleSourceDestinationLimitDebugReport(
266                         source,
267                         String.valueOf(
268                                 mFlags.getMeasurementMaxDistinctDestinationsInActiveSource()),
269                         dao);
270                 adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_LIMIT);
271                 return InsertSourcePermission.NOT_ALLOWED;
272             }
273 
274             if (appDestSourceAllowedToInsert == InsertSourcePermission.ALLOWED_FIFO_SUCCESS
275                     || webDestSourceAllowedToInsert
276                             == InsertSourcePermission.ALLOWED_FIFO_SUCCESS) {
277                 int limit = mFlags.getMeasurementMaxDistinctDestinationsInActiveSource();
278                 additionalDebugReportParams =
279                         Map.of(DebugReportApi.Body.SOURCE_DESTINATION_LIMIT, String.valueOf(limit));
280                 result = InsertSourcePermission.ALLOWED_FIFO_SUCCESS;
281                 adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_LIMIT_REPLACED);
282             }
283         }
284 
285         // Global (cross ad-tech) destinations rate limit
286         if (destinationExceedsGlobalRateLimit) {
287             // Source won't be inserted, yet we produce a success to debug report to avoid side
288             // channel leakage of cross site data
289             mDebugReportApi.scheduleSourceReport(
290                     source,
291                     source.getAttributionMode() != Source.AttributionMode.TRUTHFULLY
292                             ? DebugReportApi.Type.SOURCE_NOISED
293                             : DebugReportApi.Type.SOURCE_SUCCESS,
294                     additionalDebugReportParams,
295                     dao);
296             adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_GLOBAL_RATE_LIMIT);
297             return InsertSourcePermission.NOT_ALLOWED;
298         }
299 
300         int numOfDistinctOriginExcludingRegistrationOrigin =
301                 dao.countDistinctRegOriginPerPublisherXEnrollmentExclRegOrigin(
302                         source.getRegistrationOrigin(),
303                         publisher.get(),
304                         publisherType,
305                         source.getEnrollmentId(),
306                         source.getEventTime(),
307                         mFlags.getMeasurementMinReportingOriginUpdateWindow());
308         if (numOfDistinctOriginExcludingRegistrationOrigin
309                 >= mFlags.getMeasurementMaxReportingOriginsPerSourceReportingSitePerWindow()) {
310             LoggerFactory.getMeasurementLogger()
311                     .d(
312                             "storeSource (FAILURE): Reached limit of %s reporting origin for"
313                                     + " publisher - %s and enrollment - %s per window."
314                                     + " Source ID: %s, Source Event ID: %s",
315                             mFlags
316                                 .getMeasurementMaxReportingOriginsPerSourceReportingSitePerWindow(),
317                             publisher,
318                             source.getEnrollmentId(),
319                             source.getId(),
320                             source.getEventId());
321             mDebugReportApi.scheduleSourceReport(
322                     source,
323                     source.getAttributionMode() != Source.AttributionMode.TRUTHFULLY
324                             ? DebugReportApi.Type.SOURCE_NOISED
325                             : DebugReportApi.Type.SOURCE_SUCCESS,
326                     additionalDebugReportParams,
327                     dao);
328             adrTypes.add(DebugReportApi.Type.SOURCE_REPORTING_ORIGIN_PER_SITE_LIMIT);
329             return InsertSourcePermission.NOT_ALLOWED;
330         }
331 
332         LoggerFactory.getMeasurementLogger()
333                 .d(
334                         "storeSource: Source allowed to be inserted. Enrollment ID: %s, "
335                                 + "Source ID: %s, Source Event ID: %s",
336                         source.getEnrollmentId(), source.getId(), source.getEventId());
337         return result;
338     }
339 
isNavigationOriginAlreadyRegisteredForRegistration( @onNull Source source, IMeasurementDao dao)340     private boolean isNavigationOriginAlreadyRegisteredForRegistration(
341             @NonNull Source source, IMeasurementDao dao) throws DatastoreException {
342         if (!mFlags.getMeasurementEnableNavigationReportingOriginCheck()
343                 || source.getSourceType() != Source.SourceType.NAVIGATION) {
344             return false;
345         }
346         return dao.countNavigationSourcesPerReportingOrigin(
347                         source.getRegistrationOrigin(), source.getRegistrationId())
348                 > 0;
349     }
350 
getTopLevelPublisher( Uri topOrigin, @EventSurfaceType int publisherType)351     private static Optional<Uri> getTopLevelPublisher(
352             Uri topOrigin, @EventSurfaceType int publisherType) {
353         return publisherType == EventSurfaceType.APP
354                 ? Optional.of(topOrigin)
355                 : WebAddresses.topPrivateDomainAndScheme(topOrigin);
356     }
357 
sourceExceedsTimeBasedDestinationLimits( Source source, Uri publisher, @EventSurfaceType int publisherType, long window, int limit, IMeasurementDao dao)358     private static boolean sourceExceedsTimeBasedDestinationLimits(
359             Source source,
360             Uri publisher,
361             @EventSurfaceType int publisherType,
362             long window,
363             int limit,
364             IMeasurementDao dao)
365             throws DatastoreException {
366         List<Uri> appDestinations = source.getAppDestinations();
367         if (appDestinations != null) {
368             int appDestinationReportingCount =
369                     dao.countDistinctDestPerPubXEnrollmentInUnexpiredSourceInWindow(
370                             publisher,
371                             publisherType,
372                             source.getEnrollmentId(),
373                             appDestinations,
374                             EventSurfaceType.APP,
375                             /* window start time */ source.getEventTime() - window,
376                             /*window end time*/ source.getEventTime());
377             // Same reporting-site destination limit
378             if (appDestinationReportingCount + appDestinations.size() > limit) {
379                 LoggerFactory.getMeasurementLogger()
380                         .d(
381                                 "AsyncRegistrationQueueRunner: App time based destination limit"
382                                         + " exceeded");
383                 return true;
384             }
385         }
386 
387         List<Uri> webDestinations = source.getWebDestinations();
388         if (webDestinations != null) {
389             int webDestinationReportingCount =
390                     dao.countDistinctDestPerPubXEnrollmentInUnexpiredSourceInWindow(
391                             publisher,
392                             publisherType,
393                             source.getEnrollmentId(),
394                             webDestinations,
395                             EventSurfaceType.WEB,
396                             /* window start time */ source.getEventTime() - window,
397                             /*window end time*/ source.getEventTime());
398 
399             // Same reporting-site destination limit
400             if (webDestinationReportingCount + webDestinations.size() > limit) {
401                 LoggerFactory.getMeasurementLogger()
402                         .d(
403                                 "AsyncRegistrationQueueRunner: Web time based destination limit"
404                                         + " exceeded");
405                 return true;
406             }
407         }
408 
409         return false;
410     }
411 
destinationExceedsGlobalRateLimit( Source source, Uri publisher, IMeasurementDao dao)412     private boolean destinationExceedsGlobalRateLimit(
413             Source source, Uri publisher, IMeasurementDao dao) throws DatastoreException {
414         long window = mFlags.getMeasurementDestinationRateLimitWindow();
415         long limit = mFlags.getMeasurementMaxDestinationsPerPublisherPerRateLimitWindow();
416         long windowStartTime = source.getEventTime() - window;
417         List<Uri> appDestinations = source.getAppDestinations();
418         if (appDestinations != null) {
419             int destinationCount =
420                     dao.countDistinctDestinationsPerPublisherPerRateLimitWindow(
421                             publisher,
422                             source.getPublisherType(),
423                             /* excluded destinations */ appDestinations,
424                             EventSurfaceType.APP,
425                             windowStartTime,
426                             /* windowEndTime */ source.getEventTime());
427 
428             if (destinationCount + appDestinations.size() > limit) {
429                 LoggerFactory.getMeasurementLogger()
430                         .d(
431                                 "AsyncRegistrationQueueRunner: App destination global rate limit "
432                                         + "exceeded");
433                 return true;
434             }
435         }
436 
437         List<Uri> webDestinations = source.getWebDestinations();
438         if (webDestinations != null) {
439             int destinationCount =
440                     dao.countDistinctDestinationsPerPublisherPerRateLimitWindow(
441                             publisher,
442                             source.getPublisherType(),
443                             /* excluded destinations */ webDestinations,
444                             EventSurfaceType.WEB,
445                             windowStartTime,
446                             /* windowEndTime */ source.getEventTime());
447 
448             if (destinationCount + webDestinations.size() > limit) {
449                 LoggerFactory.getMeasurementLogger()
450                         .d(
451                                 "AsyncRegistrationQueueRunner: Web destination global rate limit "
452                                         + "exceeded");
453                 return true;
454             }
455         }
456 
457         return false;
458     }
459 
isDestinationOutOfBounds( Source source, Uri publisher, @EventSurfaceType int publisherType, String enrollmentId, List<Uri> destinations, @EventSurfaceType int destinationType, long windowStartTime, long requestTime, IMeasurementDao dao, Set<DebugReportApi.Type> adrTypes)460     private boolean isDestinationOutOfBounds(
461             Source source,
462             Uri publisher,
463             @EventSurfaceType int publisherType,
464             String enrollmentId,
465             List<Uri> destinations,
466             @EventSurfaceType int destinationType,
467             long windowStartTime,
468             long requestTime,
469             IMeasurementDao dao,
470             Set<DebugReportApi.Type> adrTypes)
471             throws DatastoreException {
472         Flags flags = FlagsFactory.getFlags();
473 
474         // If the source has destination algorithm overridden as LIFO, the source is rejected if the
475         // destination rate limit is exceeded.
476         if (extractSourceDestinationLimitingAlgo(flags, source)
477                 == Source.DestinationLimitAlgorithm.LIFO) {
478             int destinationCount;
479             if (flags.getMeasurementEnableDestinationRateLimit()) {
480                 destinationCount =
481                         dao.countDistinctDestinationsPerPubXEnrollmentInUnexpiredSource(
482                                 publisher,
483                                 publisherType,
484                                 enrollmentId,
485                                 destinations,
486                                 destinationType,
487                                 requestTime);
488             } else {
489                 destinationCount =
490                         dao.countDistinctDestPerPubXEnrollmentInUnexpiredSourceInWindow(
491                                 publisher,
492                                 publisherType,
493                                 enrollmentId,
494                                 destinations,
495                                 destinationType,
496                                 windowStartTime,
497                                 requestTime);
498             }
499             int maxDistinctDestinations =
500                     flags.getMeasurementMaxDistinctDestinationsInActiveSource();
501             if (destinationCount + destinations.size() > maxDistinctDestinations) {
502                 LoggerFactory.getMeasurementLogger()
503                         .d(
504                                 "AsyncRegistrationQueueRunner: "
505                                         + (destinationType == EventSurfaceType.APP ? "App" : "Web")
506                                         + " destination count >="
507                                         + " MaxDistinctDestinationsPerPublisherXEnrollmentInActive"
508                                         + "Source. Enrollment ID: "
509                                         + source.getEnrollmentId()
510                                         + ", Source ID: "
511                                         + source.getId()
512                                         + ", Source Event ID: "
513                                         + source.getEventId());
514                 mDebugReportApi.scheduleSourceDestinationLimitDebugReport(
515                         source, String.valueOf(maxDistinctDestinations), dao);
516                 adrTypes.add(DebugReportApi.Type.SOURCE_DESTINATION_LIMIT);
517                 return true;
518             }
519         }
520 
521         int distinctReportingOriginCount =
522                 dao.countDistinctReportingOriginsPerPublisherXDestinationInSource(
523                         publisher,
524                         publisherType,
525                         destinations,
526                         source.getRegistrationOrigin(),
527                         windowStartTime,
528                         requestTime);
529         if (distinctReportingOriginCount
530                 >= flags.getMeasurementMaxDistinctRepOrigPerPublXDestInSource()) {
531             LoggerFactory.getMeasurementLogger()
532                     .d(
533                             "AsyncRegistrationQueueRunner: "
534                                     + (destinationType == EventSurfaceType.APP ? "App" : "Web")
535                                     + " distinct reporting origin count >= "
536                                     + "MaxDistinctRepOrigPerPublisherXDestInSource exceeded."
537                                     + " Enrollment ID: "
538                                     + source.getEnrollmentId()
539                                     + ", Source ID: "
540                                     + source.getId()
541                                     + ", Source Event ID: "
542                                     + source.getEventId());
543             mDebugReportApi.scheduleSourceReport(
544                     source,
545                     source.getAttributionMode() != Source.AttributionMode.TRUTHFULLY
546                             ? DebugReportApi.Type.SOURCE_NOISED
547                             : DebugReportApi.Type.SOURCE_SUCCESS,
548                     null,
549                     dao);
550             adrTypes.add(DebugReportApi.Type.SOURCE_REPORTING_ORIGIN_LIMIT);
551             return true;
552         }
553         return false;
554     }
555 
556     /**
557      * Returns the effective source destination limiting algorithm. Return if the source has
558      * overridden the algorithm, otherwise fallback to the configured default destination algorithm.
559      *
560      * @param flags flags
561      * @param source incoming source
562      * @return the effective source destination limiting algorithm
563      */
extractSourceDestinationLimitingAlgo( Flags flags, Source source)564     private static Source.DestinationLimitAlgorithm extractSourceDestinationLimitingAlgo(
565             Flags flags, Source source) {
566         return Optional.ofNullable(source.getDestinationLimitAlgorithm())
567                 .orElse(
568                         Source.DestinationLimitAlgorithm.values()[
569                                 flags.getMeasurementDefaultSourceDestinationLimitAlgorithm()]);
570     }
571 
deleteLowPriorityDestinationSourcesToAccommodateNewSource( Source source, @EventSurfaceType int publisherType, IMeasurementDao dao, Uri publisher, @EventSurfaceType int destinationType, List<Uri> destinations, AsyncFetchStatus asyncFetchStatus)572     private InsertSourcePermission deleteLowPriorityDestinationSourcesToAccommodateNewSource(
573             Source source,
574             @EventSurfaceType int publisherType,
575             IMeasurementDao dao,
576             Uri publisher,
577             @EventSurfaceType int destinationType,
578             List<Uri> destinations,
579             AsyncFetchStatus asyncFetchStatus)
580             throws DatastoreException {
581         if (destinations == null || destinations.isEmpty()) {
582             return InsertSourcePermission.ALLOWED;
583         }
584         int fifoLimit = mFlags.getMeasurementMaxDistinctDestinationsInActiveSource();
585         if (destinations.size() > fifoLimit) {
586             return InsertSourcePermission.NOT_ALLOWED;
587         }
588         int distinctDestinations =
589                 dao.countDistinctDestinationsPerPubXEnrollmentInUnexpiredSource(
590                         publisher,
591                         publisherType,
592                         source.getEnrollmentId(),
593                         destinations,
594                         destinationType,
595                         source.getEventTime());
596         if (distinctDestinations + destinations.size() <= fifoLimit) {
597             // Source is allowed to be inserted without any deletion
598             return InsertSourcePermission.ALLOWED;
599         }
600 
601         // Delete sources associated to the oldest destination per enrollment per publisher.
602         // The new source may have multiple app and web destination, because of which we might
603         // need to delete multiple oldest destinations - in FIFO manner, i.e. in a loop.
604         // Although it should not be more than 4 iterations because the new source can have
605         // at max 1 app destination and 3 web destinations (configurable).
606         while (distinctDestinations + destinations.size() > fifoLimit) {
607             // Delete sources for the lowest priority / oldest destination
608             Pair<Long, List<String>> destinationPriorityWithSourcesToDelete =
609                     dao.fetchSourceIdsForLowestPriorityDestinationXEnrollmentXPublisher(
610                             publisher,
611                             publisherType,
612                             source.getEnrollmentId(),
613                             destinations,
614                             destinationType,
615                             source.getEventTime());
616             if (source.getDestinationLimitPriority()
617                     < destinationPriorityWithSourcesToDelete.first) {
618                 // If the incoming source has a lower priority than the least prioritized
619                 // destination, reject the incoming source.
620                 return InsertSourcePermission.NOT_ALLOWED;
621             }
622 
623             List<String> sourceIdsToDelete = destinationPriorityWithSourcesToDelete.second;
624             if (sourceIdsToDelete.isEmpty()) {
625                 // If destination limit exceeds, the oldest destination deletion should be
626                 // successful. This is an unexpected state.
627                 throw new IllegalStateException(
628                         "No sources were deleted; incoming destinations: "
629                                 + destinations.size()
630                                 + "; FIFO limit:"
631                                 + fifoLimit);
632             }
633             dao.updateSourceStatus(sourceIdsToDelete, Source.Status.MARKED_TO_DELETE);
634             LoggerFactory.getMeasurementLogger()
635                     .d(
636                             "Deleted "
637                                     + sourceIdsToDelete.size()
638                                     + " sources to insert the new source.");
639             if (mFlags.getMeasurementEnableFifoDestinationsDeleteAggregateReports()) {
640                 dao.deletePendingAggregateReportsAndAttributionsForSources(sourceIdsToDelete);
641                 LoggerFactory.getMeasurementLogger()
642                         .d(
643                                 "Deleted pending aggregate reports of"
644                                         + sourceIdsToDelete.size()
645                                         + " sources to insert the new source.");
646             }
647             dao.deleteFutureFakeEventReportsForSources(sourceIdsToDelete, source.getEventTime());
648             distinctDestinations =
649                     dao.countDistinctDestinationsPerPubXEnrollmentInUnexpiredSource(
650                             publisher,
651                             publisherType,
652                             source.getEnrollmentId(),
653                             destinations,
654                             destinationType,
655                             source.getEventTime());
656             asyncFetchStatus.incrementNumDeletedEntities(sourceIdsToDelete.size());
657         }
658         return InsertSourcePermission.ALLOWED_FIFO_SUCCESS;
659     }
660 }
661