• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.service.adselection;
18 
19 import static com.android.adservices.service.stats.AdServicesLoggerUtil.getResultCodeFromException;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS;
21 
22 import android.adservices.adselection.AdSelectionCallback;
23 import android.adservices.adselection.AdSelectionConfig;
24 import android.adservices.adselection.AdSelectionInput;
25 import android.adservices.adselection.AdSelectionResponse;
26 import android.adservices.adselection.ContextualAds;
27 import android.adservices.common.AdServicesStatusUtils;
28 import android.adservices.common.AdTechIdentifier;
29 import android.adservices.common.FledgeErrorResponse;
30 import android.adservices.exceptions.AdServicesException;
31 import android.annotation.NonNull;
32 import android.annotation.Nullable;
33 import android.content.Context;
34 import android.net.Uri;
35 import android.os.Build;
36 import android.os.RemoteException;
37 import android.os.Trace;
38 
39 import androidx.annotation.RequiresApi;
40 
41 import com.android.adservices.LoggerFactory;
42 import com.android.adservices.data.adselection.AdSelectionEntryDao;
43 import com.android.adservices.data.adselection.DBAdSelection;
44 import com.android.adservices.data.adselection.DBBuyerDecisionLogic;
45 import com.android.adservices.data.customaudience.CustomAudienceDao;
46 import com.android.adservices.data.customaudience.DBCustomAudience;
47 import com.android.adservices.service.Flags;
48 import com.android.adservices.service.common.AdSelectionServiceFilter;
49 import com.android.adservices.service.common.Throttler;
50 import com.android.adservices.service.consent.ConsentManager;
51 import com.android.adservices.service.exception.FilterException;
52 import com.android.adservices.service.js.JSScriptEngine;
53 import com.android.adservices.service.profiling.Tracing;
54 import com.android.adservices.service.stats.AdSelectionExecutionLogger;
55 import com.android.adservices.service.stats.AdServicesLogger;
56 import com.android.adservices.service.stats.AdServicesLoggerUtil;
57 import com.android.adservices.service.stats.AdServicesStatsLog;
58 import com.android.internal.annotations.VisibleForTesting;
59 
60 import com.google.common.base.Preconditions;
61 import com.google.common.util.concurrent.AsyncFunction;
62 import com.google.common.util.concurrent.FluentFuture;
63 import com.google.common.util.concurrent.FutureCallback;
64 import com.google.common.util.concurrent.Futures;
65 import com.google.common.util.concurrent.ListenableFuture;
66 import com.google.common.util.concurrent.ListeningExecutorService;
67 import com.google.common.util.concurrent.MoreExecutors;
68 import com.google.common.util.concurrent.UncheckedTimeoutException;
69 
70 import java.time.Clock;
71 import java.util.Collections;
72 import java.util.HashMap;
73 import java.util.List;
74 import java.util.Map;
75 import java.util.Objects;
76 import java.util.concurrent.ExecutorService;
77 import java.util.concurrent.ScheduledThreadPoolExecutor;
78 import java.util.concurrent.TimeUnit;
79 import java.util.concurrent.TimeoutException;
80 import java.util.stream.Collectors;
81 
82 /**
83  * Orchestrator that runs the Ads Auction/Bidding and Scoring logic The class expects the caller to
84  * create a concrete object instance of the class. The instances are mutually exclusive and do not
85  * share any values across shared class instance.
86  *
87  * <p>Class takes in an executor on which it runs the AdSelection logic
88  */
89 // TODO(b/269798827): Enable for R.
90 @RequiresApi(Build.VERSION_CODES.S)
91 public abstract class AdSelectionRunner {
92     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
93 
94     @VisibleForTesting static final String AD_SELECTION_ERROR_PATTERN = "%s: %s";
95 
96     @VisibleForTesting
97     static final String ERROR_AD_SELECTION_FAILURE = "Encountered failure during Ad Selection";
98 
99     @VisibleForTesting static final String ERROR_NO_WINNING_AD_FOUND = "No winning Ads found";
100 
101     @VisibleForTesting
102     static final String ERROR_NO_VALID_BIDS_OR_CONTEXTUAL_ADS_FOR_SCORING =
103             "No valid bids or contextual ads available for scoring";
104 
105     @VisibleForTesting
106     static final String ERROR_NO_CA_AND_CONTEXTUAL_ADS_AVAILABLE =
107             "No Custom Audience or contextual ads available";
108 
109     @VisibleForTesting
110     static final String ERROR_NO_BUYERS_OR_CONTEXTUAL_ADS_AVAILABLE =
111             "The list of the custom audience buyers and contextual ads both should not be empty.";
112 
113     @VisibleForTesting
114     static final String AD_SELECTION_TIMED_OUT = "Ad selection exceeded allowed time limit";
115 
116     @VisibleForTesting
117     static final String JS_SANDBOX_IS_NOT_AVAILABLE =
118             String.format(
119                     AD_SELECTION_ERROR_PATTERN,
120                     ERROR_AD_SELECTION_FAILURE,
121                     "JS Sandbox is not available");
122 
123     @NonNull protected final CustomAudienceDao mCustomAudienceDao;
124     @NonNull protected final AdSelectionEntryDao mAdSelectionEntryDao;
125     @NonNull protected final ListeningExecutorService mLightweightExecutorService;
126     @NonNull protected final ListeningExecutorService mBackgroundExecutorService;
127     @NonNull protected final ScheduledThreadPoolExecutor mScheduledExecutor;
128     @NonNull protected final AdSelectionIdGenerator mAdSelectionIdGenerator;
129     @NonNull protected final Clock mClock;
130     @NonNull protected final AdServicesLogger mAdServicesLogger;
131     @NonNull protected final Flags mFlags;
132     @NonNull protected final AdSelectionExecutionLogger mAdSelectionExecutionLogger;
133     @NonNull private final AdSelectionServiceFilter mAdSelectionServiceFilter;
134     @NonNull private final AdFilterer mAdFilterer;
135     private final int mCallerUid;
136     @NonNull private final PrebuiltLogicGenerator mPrebuiltLogicGenerator;
137 
138     /**
139      * @param context service context
140      * @param customAudienceDao DAO to access custom audience storage
141      * @param adSelectionEntryDao DAO to access ad selection storage
142      * @param lightweightExecutorService executor for running short tasks
143      * @param backgroundExecutorService executor for longer running tasks (ex. network calls)
144      * @param scheduledExecutor executor for tasks to be run with a delay or timed executions
145      * @param adServicesLogger logger for logging calls to PPAPI
146      * @param flags for accessing feature flags
147      * @param adSelectionServiceFilter for validating the request
148      */
AdSelectionRunner( @onNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @NonNull final AdServicesLogger adServicesLogger, @NonNull final Flags flags, @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger, @NonNull final AdSelectionServiceFilter adSelectionServiceFilter, @NonNull final AdFilterer adFilterer, @NonNull final int callerUid)149     public AdSelectionRunner(
150             @NonNull final Context context,
151             @NonNull final CustomAudienceDao customAudienceDao,
152             @NonNull final AdSelectionEntryDao adSelectionEntryDao,
153             @NonNull final ExecutorService lightweightExecutorService,
154             @NonNull final ExecutorService backgroundExecutorService,
155             @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
156             @NonNull final AdServicesLogger adServicesLogger,
157             @NonNull final Flags flags,
158             @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger,
159             @NonNull final AdSelectionServiceFilter adSelectionServiceFilter,
160             @NonNull final AdFilterer adFilterer,
161             @NonNull final int callerUid) {
162         Objects.requireNonNull(context);
163         Objects.requireNonNull(customAudienceDao);
164         Objects.requireNonNull(adSelectionEntryDao);
165         Objects.requireNonNull(lightweightExecutorService);
166         Objects.requireNonNull(backgroundExecutorService);
167         Objects.requireNonNull(adServicesLogger);
168         Objects.requireNonNull(flags);
169         Objects.requireNonNull(adSelectionServiceFilter);
170         Objects.requireNonNull(adFilterer);
171 
172         Preconditions.checkArgument(
173                 JSScriptEngine.AvailabilityChecker.isJSSandboxAvailable(),
174                 JS_SANDBOX_IS_NOT_AVAILABLE);
175         Objects.requireNonNull(adSelectionExecutionLogger);
176 
177         mCustomAudienceDao = customAudienceDao;
178         mAdSelectionEntryDao = adSelectionEntryDao;
179         mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService);
180         mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService);
181         mScheduledExecutor = scheduledExecutor;
182         mAdServicesLogger = adServicesLogger;
183         mAdSelectionIdGenerator = new AdSelectionIdGenerator();
184         mClock = Clock.systemUTC();
185         mFlags = flags;
186         mAdSelectionExecutionLogger = adSelectionExecutionLogger;
187         mAdSelectionServiceFilter = adSelectionServiceFilter;
188         mAdFilterer = adFilterer;
189         mCallerUid = callerUid;
190         mPrebuiltLogicGenerator = new PrebuiltLogicGenerator(mFlags);
191     }
192 
193     @VisibleForTesting
AdSelectionRunner( @onNull final Context context, @NonNull final CustomAudienceDao customAudienceDao, @NonNull final AdSelectionEntryDao adSelectionEntryDao, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @NonNull final AdSelectionIdGenerator adSelectionIdGenerator, @NonNull Clock clock, @NonNull final AdServicesLogger adServicesLogger, @NonNull final Flags flags, int callerUid, @NonNull AdSelectionServiceFilter adSelectionServiceFilter, @NonNull AdFilterer adFilterer, @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger)194     AdSelectionRunner(
195             @NonNull final Context context,
196             @NonNull final CustomAudienceDao customAudienceDao,
197             @NonNull final AdSelectionEntryDao adSelectionEntryDao,
198             @NonNull final ExecutorService lightweightExecutorService,
199             @NonNull final ExecutorService backgroundExecutorService,
200             @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
201             @NonNull final AdSelectionIdGenerator adSelectionIdGenerator,
202             @NonNull Clock clock,
203             @NonNull final AdServicesLogger adServicesLogger,
204             @NonNull final Flags flags,
205             int callerUid,
206             @NonNull AdSelectionServiceFilter adSelectionServiceFilter,
207             @NonNull AdFilterer adFilterer,
208             @NonNull final AdSelectionExecutionLogger adSelectionExecutionLogger) {
209         Objects.requireNonNull(context);
210         Objects.requireNonNull(customAudienceDao);
211         Objects.requireNonNull(adSelectionEntryDao);
212         Objects.requireNonNull(lightweightExecutorService);
213         Objects.requireNonNull(backgroundExecutorService);
214         Objects.requireNonNull(scheduledExecutor);
215         Objects.requireNonNull(adSelectionIdGenerator);
216         Objects.requireNonNull(clock);
217         Objects.requireNonNull(adServicesLogger);
218         Objects.requireNonNull(flags);
219         Objects.requireNonNull(adSelectionExecutionLogger);
220         Objects.requireNonNull(adFilterer);
221 
222         Preconditions.checkArgument(
223                 JSScriptEngine.AvailabilityChecker.isJSSandboxAvailable(),
224                 JS_SANDBOX_IS_NOT_AVAILABLE);
225 
226         mCustomAudienceDao = customAudienceDao;
227         mAdSelectionEntryDao = adSelectionEntryDao;
228         mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService);
229         mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService);
230         mScheduledExecutor = scheduledExecutor;
231         mAdSelectionIdGenerator = adSelectionIdGenerator;
232         mClock = clock;
233         mAdServicesLogger = adServicesLogger;
234         mFlags = flags;
235         mAdSelectionExecutionLogger = adSelectionExecutionLogger;
236         mAdSelectionServiceFilter = adSelectionServiceFilter;
237         mAdFilterer = adFilterer;
238         mCallerUid = callerUid;
239         mPrebuiltLogicGenerator = new PrebuiltLogicGenerator(mFlags);
240     }
241 
242     /**
243      * Runs the ad selection for a given seller
244      *
245      * @param inputParams containing {@link AdSelectionConfig} and {@code callerPackageName}
246      * @param callback used to notify the result back to the calling seller
247      */
runAdSelection( @onNull AdSelectionInput inputParams, @NonNull AdSelectionCallback callback)248     public void runAdSelection(
249             @NonNull AdSelectionInput inputParams, @NonNull AdSelectionCallback callback) {
250         final int traceCookie = Tracing.beginAsyncSection(Tracing.RUN_AD_SELECTION);
251         Objects.requireNonNull(inputParams);
252         Objects.requireNonNull(callback);
253         AdSelectionConfig adSelectionConfig = inputParams.getAdSelectionConfig();
254 
255         try {
256             ListenableFuture<Void> filterAndValidateRequestFuture =
257                     Futures.submit(
258                             () -> {
259                                 try {
260                                     Trace.beginSection(Tracing.VALIDATE_REQUEST);
261                                     sLogger.v("Starting filtering and validation.");
262                                     mAdSelectionServiceFilter.filterRequest(
263                                             adSelectionConfig.getSeller(),
264                                             inputParams.getCallerPackageName(),
265                                             mFlags
266                                                     .getEnforceForegroundStatusForFledgeRunAdSelection(),
267                                             true,
268                                             mCallerUid,
269                                             AdServicesStatsLog
270                                                     .AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
271                                             Throttler.ApiKey.FLEDGE_API_SELECT_ADS);
272                                     validateAdSelectionConfig(adSelectionConfig);
273                                 } finally {
274                                     sLogger.v("Completed filtering and validation.");
275                                     Trace.endSection();
276                                 }
277                             },
278                             mLightweightExecutorService);
279 
280             ListenableFuture<DBAdSelection> dbAdSelectionFuture =
281                     FluentFuture.from(filterAndValidateRequestFuture)
282                             .transformAsync(
283                                     ignoredVoid ->
284                                             orchestrateAdSelection(
285                                                     inputParams.getAdSelectionConfig(),
286                                                     inputParams.getCallerPackageName()),
287                                     mLightweightExecutorService)
288                             .transform(
289                                     this::closeSuccessfulAdSelection, mLightweightExecutorService)
290                             .catching(
291                                     RuntimeException.class,
292                                     this::closeFailedAdSelectionWithRuntimeException,
293                                     mLightweightExecutorService)
294                             .catching(
295                                     AdServicesException.class,
296                                     this::closeFailedAdSelectionWithAdServicesException,
297                                     mLightweightExecutorService);
298 
299             Futures.addCallback(
300                     dbAdSelectionFuture,
301                     new FutureCallback<DBAdSelection>() {
302                         @Override
303                         public void onSuccess(DBAdSelection result) {
304                             Tracing.endAsyncSection(Tracing.RUN_AD_SELECTION, traceCookie);
305                             notifySuccessToCaller(result, callback);
306                         }
307 
308                         @Override
309                         public void onFailure(Throwable t) {
310                             Tracing.endAsyncSection(Tracing.RUN_AD_SELECTION, traceCookie);
311                             if (t instanceof FilterException
312                                     && t.getCause()
313                                             instanceof ConsentManager.RevokedConsentException) {
314                                 // Skip logging if a FilterException occurs.
315                                 // AdSelectionServiceFilter ensures the failing assertion is logged
316                                 // internally.
317 
318                                 // Fail Silently by notifying success to caller
319                                 notifyEmptySuccessToCaller(callback);
320                             } else {
321                                 if (t.getCause() instanceof AdServicesException) {
322                                     notifyFailureToCaller(callback, t.getCause());
323                                 } else {
324                                     notifyFailureToCaller(callback, t);
325                                 }
326                             }
327                         }
328                     },
329                     mLightweightExecutorService);
330         } catch (Throwable t) {
331             Tracing.endAsyncSection(Tracing.RUN_AD_SELECTION, traceCookie);
332             sLogger.v("run ad selection fails fast with exception %s.", t.toString());
333             notifyFailureToCaller(callback, t);
334         }
335     }
336 
337     @Nullable
closeFailedAdSelectionWithRuntimeException(RuntimeException e)338     private DBAdSelection closeFailedAdSelectionWithRuntimeException(RuntimeException e) {
339         sLogger.v("Close failed ad selection and rethrow the RuntimeException %s.", e.toString());
340         int resultCode = AdServicesLoggerUtil.getResultCodeFromException(e);
341         mAdSelectionExecutionLogger.close(resultCode);
342         throw e;
343     }
344 
345     @Nullable
closeFailedAdSelectionWithAdServicesException(AdServicesException e)346     private DBAdSelection closeFailedAdSelectionWithAdServicesException(AdServicesException e) {
347         int resultCode = AdServicesLoggerUtil.getResultCodeFromException(e);
348         mAdSelectionExecutionLogger.close(resultCode);
349         sLogger.v(
350                 "Close failed ad selection and wrap the AdServicesException with"
351                         + " an RuntimeException with message: %s and log with resultCode : %d",
352                 e.getMessage(), resultCode);
353         throw new RuntimeException(e.getMessage(), e.getCause());
354     }
355 
356     @NonNull
closeSuccessfulAdSelection(@onNull DBAdSelection dbAdSelection)357     private DBAdSelection closeSuccessfulAdSelection(@NonNull DBAdSelection dbAdSelection) {
358         mAdSelectionExecutionLogger.close(AdServicesStatusUtils.STATUS_SUCCESS);
359         return dbAdSelection;
360     }
361 
notifySuccessToCaller( @onNull DBAdSelection result, @NonNull AdSelectionCallback callback)362     private void notifySuccessToCaller(
363             @NonNull DBAdSelection result, @NonNull AdSelectionCallback callback) {
364         try {
365             int overallLatencyMs =
366                     mAdSelectionExecutionLogger.getRunAdSelectionOverallLatencyInMs();
367             sLogger.v(
368                     "Ad Selection with Id:%d completed with overall latency %d in ms, "
369                             + "attempted notifying success",
370                     result.getAdSelectionId(), overallLatencyMs);
371             // TODO(b//253522566): When including logging data from bidding & auction server side
372             //  should be able to differentiate the data from the on-device telemetry.
373             // Note: Success is logged before the callback to ensure deterministic testing.
374             mAdServicesLogger.logFledgeApiCallStats(
375                     AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS,
376                     AdServicesStatusUtils.STATUS_SUCCESS,
377                     overallLatencyMs);
378 
379             callback.onSuccess(
380                     new AdSelectionResponse.Builder()
381                             .setAdSelectionId(result.getAdSelectionId())
382                             .setRenderUri(result.getWinningAdRenderUri())
383                             .build());
384         } catch (RemoteException e) {
385             sLogger.e(e, "Encountered exception during notifying AdSelection callback");
386         }
387     }
388 
389     /** Sends a successful response to the caller that represents a silent failure. */
notifyEmptySuccessToCaller(@onNull AdSelectionCallback callback)390     private void notifyEmptySuccessToCaller(@NonNull AdSelectionCallback callback) {
391         try {
392             callback.onSuccess(
393                     new AdSelectionResponse.Builder()
394                             .setAdSelectionId(mAdSelectionIdGenerator.generateId())
395                             .setRenderUri(Uri.EMPTY)
396                             .build());
397         } catch (RemoteException e) {
398             sLogger.e(e, "Encountered exception during notifying AdSelection callback");
399         }
400     }
401 
notifyFailureToCaller( @onNull AdSelectionCallback callback, @NonNull Throwable t)402     private void notifyFailureToCaller(
403             @NonNull AdSelectionCallback callback, @NonNull Throwable t) {
404         try {
405             sLogger.e(t, "Ad Selection failure: ");
406 
407             int resultCode = AdServicesLoggerUtil.getResultCodeFromException(t);
408 
409             // Skip logging if a FilterException occurs.
410             // AdSelectionServiceFilter ensures the failing assertion is logged internally.
411             // Note: Failure is logged before the callback to ensure deterministic testing.
412             if (!(t instanceof FilterException)) {
413                 int overallLatencyMs =
414                         mAdSelectionExecutionLogger.getRunAdSelectionOverallLatencyInMs();
415                 sLogger.v("Ad Selection failed with overall latency %d in ms", overallLatencyMs);
416                 // TODO(b//253522566): When including logging data from bidding & auction server
417                 // side
418                 //  should be able to differentiate the data from the on-device telemetry.
419                 mAdServicesLogger.logFledgeApiCallStats(
420                         AD_SERVICES_API_CALLED__API_NAME__SELECT_ADS, resultCode, overallLatencyMs);
421             }
422 
423             FledgeErrorResponse selectionFailureResponse =
424                     new FledgeErrorResponse.Builder()
425                             .setErrorMessage(
426                                     String.format(
427                                             AD_SELECTION_ERROR_PATTERN,
428                                             ERROR_AD_SELECTION_FAILURE,
429                                             t.getMessage()))
430                             .setStatusCode(resultCode)
431                             .build();
432             callback.onFailure(selectionFailureResponse);
433         } catch (RemoteException e) {
434             sLogger.e(e, "Encountered exception during notifying AdSelection callback");
435         }
436     }
437 
438     /**
439      * Overall moderator for running Ad Selection
440      *
441      * @param adSelectionConfig Set of data from Sellers and Buyers needed for Ad Auction and
442      *     Selection
443      * @return {@link AdSelectionResponse}
444      */
orchestrateAdSelection( @onNull final AdSelectionConfig adSelectionConfig, @NonNull final String callerPackageName)445     private ListenableFuture<DBAdSelection> orchestrateAdSelection(
446             @NonNull final AdSelectionConfig adSelectionConfig,
447             @NonNull final String callerPackageName) {
448         sLogger.v("Beginning Ad Selection Orchestration");
449 
450         AdSelectionConfig adSelectionConfigInput = adSelectionConfig;
451         if (!mFlags.getFledgeAdSelectionContextualAdsEnabled()) {
452             // Empty all contextual ads if the feature is disabled
453             sLogger.v("Contextual flow is disabled");
454             adSelectionConfigInput = getAdSelectionConfigWithoutContextualAds(adSelectionConfig);
455         } else {
456             sLogger.v("Contextual flow is enabled, filtering contextual ads");
457             adSelectionConfigInput = getAdSelectionConfigFilterContextualAds(adSelectionConfig);
458         }
459 
460         ListenableFuture<List<DBCustomAudience>> buyerCustomAudience =
461                 getBuyersCustomAudience(adSelectionConfigInput);
462         ListenableFuture<AdSelectionOrchestrationResult> dbAdSelection =
463                 orchestrateAdSelection(
464                         adSelectionConfigInput, callerPackageName, buyerCustomAudience);
465 
466         AsyncFunction<AdSelectionOrchestrationResult, DBAdSelection> saveResultToPersistence =
467                 adSelectionAndJs ->
468                         persistAdSelection(
469                                 adSelectionAndJs.mDbAdSelectionBuilder,
470                                 adSelectionAndJs.mBuyerDecisionLogicJs,
471                                 callerPackageName);
472 
473         return FluentFuture.from(dbAdSelection)
474                 .transformAsync(saveResultToPersistence, mLightweightExecutorService)
475                 .withTimeout(
476                         mFlags.getAdSelectionOverallTimeoutMs(),
477                         TimeUnit.MILLISECONDS,
478                         mScheduledExecutor)
479                 .catching(
480                         TimeoutException.class,
481                         this::handleTimeoutError,
482                         mLightweightExecutorService);
483     }
484 
orchestrateAdSelection( @onNull AdSelectionConfig adSelectionConfig, @NonNull String callerPackageName, @NonNull ListenableFuture<List<DBCustomAudience>> buyerCustomAudience)485     abstract ListenableFuture<AdSelectionOrchestrationResult> orchestrateAdSelection(
486             @NonNull AdSelectionConfig adSelectionConfig,
487             @NonNull String callerPackageName,
488             @NonNull ListenableFuture<List<DBCustomAudience>> buyerCustomAudience);
489 
490     @Nullable
handleTimeoutError(TimeoutException e)491     private DBAdSelection handleTimeoutError(TimeoutException e) {
492         sLogger.e(e, "Ad Selection exceeded time limit");
493         throw new UncheckedTimeoutException(AD_SELECTION_TIMED_OUT);
494     }
495 
getBuyersCustomAudience( final AdSelectionConfig adSelectionConfig)496     private ListenableFuture<List<DBCustomAudience>> getBuyersCustomAudience(
497             final AdSelectionConfig adSelectionConfig) {
498         final int traceCookie = Tracing.beginAsyncSection(Tracing.GET_BUYERS_CUSTOM_AUDIENCE);
499         return mBackgroundExecutorService.submit(
500                 () -> {
501                     boolean atLeastOnePresent =
502                             !(adSelectionConfig.getCustomAudienceBuyers().isEmpty()
503                                     && adSelectionConfig.getBuyerContextualAds().isEmpty());
504 
505                     Preconditions.checkArgument(
506                             atLeastOnePresent, ERROR_NO_BUYERS_OR_CONTEXTUAL_ADS_AVAILABLE);
507                     // Set start of bidding stage.
508                     mAdSelectionExecutionLogger.startBiddingProcess(
509                             countBuyersRequested(adSelectionConfig));
510                     List<DBCustomAudience> buyerCustomAudience =
511                             mCustomAudienceDao.getActiveCustomAudienceByBuyers(
512                                     adSelectionConfig.getCustomAudienceBuyers(),
513                                     mClock.instant(),
514                                     mFlags.getFledgeCustomAudienceActiveTimeWindowInMs());
515                     if ((buyerCustomAudience == null || buyerCustomAudience.isEmpty())
516                             && adSelectionConfig.getBuyerContextualAds().isEmpty()) {
517                         IllegalStateException exception =
518                                 new IllegalStateException(ERROR_NO_CA_AND_CONTEXTUAL_ADS_AVAILABLE);
519                         mAdSelectionExecutionLogger.endBiddingProcess(
520                                 null, getResultCodeFromException(exception));
521                         throw exception;
522                     }
523                     // end a successful get-buyers-custom-audience process.
524                     mAdSelectionExecutionLogger.endGetBuyersCustomAudience(
525                             countBuyersFromCustomAudiences(buyerCustomAudience));
526                     Tracing.endAsyncSection(Tracing.GET_BUYERS_CUSTOM_AUDIENCE, traceCookie);
527                     return buyerCustomAudience;
528                 });
529     }
530 
countBuyersRequested(@onNull AdSelectionConfig adSelectionConfig)531     private int countBuyersRequested(@NonNull AdSelectionConfig adSelectionConfig) {
532         Objects.requireNonNull(adSelectionConfig);
533         return adSelectionConfig.getCustomAudienceBuyers().stream()
534                 .collect(Collectors.toSet())
535                 .size();
536     }
537 
countBuyersFromCustomAudiences( @onNull List<DBCustomAudience> buyerCustomAudience)538     private int countBuyersFromCustomAudiences(
539             @NonNull List<DBCustomAudience> buyerCustomAudience) {
540         Objects.requireNonNull(buyerCustomAudience);
541         return buyerCustomAudience.stream()
542                 .map(a -> a.getBuyer())
543                 .collect(Collectors.toSet())
544                 .size();
545     }
546 
persistAdSelection( @onNull DBAdSelection.Builder dbAdSelectionBuilder, @NonNull String buyerDecisionLogicJS, @NonNull String callerPackageName)547     private ListenableFuture<DBAdSelection> persistAdSelection(
548             @NonNull DBAdSelection.Builder dbAdSelectionBuilder,
549             @NonNull String buyerDecisionLogicJS,
550             @NonNull String callerPackageName) {
551         final int traceCookie = Tracing.beginAsyncSection(Tracing.PERSIST_AD_SELECTION);
552         return mBackgroundExecutorService.submit(
553                 () -> {
554                     long adSelectionId = mAdSelectionIdGenerator.generateId();
555                     // Retry ID generation in case of collision
556                     while (mAdSelectionEntryDao.doesAdSelectionIdExist(adSelectionId)) {
557                         adSelectionId = mAdSelectionIdGenerator.generateId();
558                     }
559                     sLogger.v("Persisting Ad Selection Result for Id:%d", adSelectionId);
560                     DBAdSelection dbAdSelection;
561                     dbAdSelectionBuilder
562                             .setAdSelectionId(adSelectionId)
563                             .setCreationTimestamp(mClock.instant())
564                             .setCallerPackageName(callerPackageName);
565                     dbAdSelection = dbAdSelectionBuilder.build();
566                     mAdSelectionExecutionLogger.startPersistAdSelection(dbAdSelection);
567                     mAdSelectionEntryDao.persistAdSelection(dbAdSelection);
568                     mAdSelectionEntryDao.persistBuyerDecisionLogic(
569                             new DBBuyerDecisionLogic.Builder()
570                                     .setBuyerDecisionLogicJs(buyerDecisionLogicJS)
571                                     .setBiddingLogicUri(dbAdSelection.getBiddingLogicUri())
572                                     .build());
573                     mAdSelectionExecutionLogger.endPersistAdSelection();
574                     Tracing.endAsyncSection(Tracing.PERSIST_AD_SELECTION, traceCookie);
575                     return dbAdSelection;
576                 });
577     }
578 
579     /**
580      * Validates the {@code adSelectionConfig} from the request.
581      *
582      * @param adSelectionConfig the adSelectionConfig to be validated
583      * @throws IllegalArgumentException if the provided {@code adSelectionConfig} is not valid
584      */
585     private void validateAdSelectionConfig(AdSelectionConfig adSelectionConfig)
586             throws IllegalArgumentException {
587         AdSelectionConfigValidator adSelectionConfigValidator =
588                 new AdSelectionConfigValidator(mPrebuiltLogicGenerator);
589         adSelectionConfigValidator.validate(adSelectionConfig);
590     }
591 
592     private AdSelectionConfig getAdSelectionConfigFilterContextualAds(
593             AdSelectionConfig adSelectionConfig) {
594         Map<AdTechIdentifier, ContextualAds> filteredContextualAdsMap = new HashMap<>();
595         sLogger.v("Filtering contextual ads in Ad Selection Config");
596         for (Map.Entry<AdTechIdentifier, ContextualAds> entry :
597                 adSelectionConfig.getBuyerContextualAds().entrySet()) {
598             filteredContextualAdsMap.put(
599                     entry.getKey(), mAdFilterer.filterContextualAds(entry.getValue()));
600         }
601         return adSelectionConfig
602                 .cloneToBuilder()
603                 .setBuyerContextualAds(filteredContextualAdsMap)
604                 .build();
605     }
606 
607     private AdSelectionConfig getAdSelectionConfigWithoutContextualAds(
608             AdSelectionConfig adSelectionConfig) {
609         sLogger.v("Emptying contextual ads in Ad Selection Config");
610         return adSelectionConfig
611                 .cloneToBuilder()
612                 .setBuyerContextualAds(Collections.EMPTY_MAP)
613                 .build();
614     }
615 
616     static class AdSelectionOrchestrationResult {
617         DBAdSelection.Builder mDbAdSelectionBuilder;
618         String mBuyerDecisionLogicJs;
619 
620         AdSelectionOrchestrationResult(
621                 DBAdSelection.Builder dbAdSelectionBuilder, String buyerDecisionLogicJs) {
622             this.mDbAdSelectionBuilder = dbAdSelectionBuilder;
623             this.mBuyerDecisionLogicJs = buyerDecisionLogicJs;
624         }
625     }
626 }
627