• 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.AdServicesStatsLog.AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN;
20 
21 import android.adservices.adselection.AdSelectionCallback;
22 import android.adservices.adselection.AdSelectionFromOutcomesConfig;
23 import android.adservices.adselection.AdSelectionFromOutcomesInput;
24 import android.adservices.adselection.AdSelectionOutcome;
25 import android.adservices.adselection.AdSelectionResponse;
26 import android.adservices.common.AdServicesStatusUtils;
27 import android.adservices.common.FledgeErrorResponse;
28 import android.adservices.exceptions.AdServicesException;
29 import android.annotation.NonNull;
30 import android.annotation.Nullable;
31 import android.content.Context;
32 import android.os.Build;
33 import android.os.RemoteException;
34 import android.os.Trace;
35 
36 import androidx.annotation.RequiresApi;
37 
38 import com.android.adservices.LoggerFactory;
39 import com.android.adservices.concurrency.AdServicesExecutors;
40 import com.android.adservices.data.adselection.AdSelectionEntryDao;
41 import com.android.adservices.service.Flags;
42 import com.android.adservices.service.common.AdSelectionServiceFilter;
43 import com.android.adservices.service.common.Throttler;
44 import com.android.adservices.service.common.cache.CacheProviderFactory;
45 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
46 import com.android.adservices.service.consent.ConsentManager;
47 import com.android.adservices.service.devapi.AdSelectionDevOverridesHelper;
48 import com.android.adservices.service.devapi.DevContext;
49 import com.android.adservices.service.exception.FilterException;
50 import com.android.adservices.service.profiling.Tracing;
51 import com.android.adservices.service.stats.AdServicesLogger;
52 import com.android.adservices.service.stats.AdServicesLoggerUtil;
53 import com.android.adservices.service.stats.AdServicesStatsLog;
54 import com.android.internal.annotations.VisibleForTesting;
55 
56 import com.google.common.util.concurrent.FluentFuture;
57 import com.google.common.util.concurrent.FutureCallback;
58 import com.google.common.util.concurrent.Futures;
59 import com.google.common.util.concurrent.ListenableFuture;
60 import com.google.common.util.concurrent.ListeningExecutorService;
61 import com.google.common.util.concurrent.MoreExecutors;
62 import com.google.common.util.concurrent.UncheckedTimeoutException;
63 
64 import java.util.ArrayList;
65 import java.util.List;
66 import java.util.Objects;
67 import java.util.concurrent.ExecutorService;
68 import java.util.concurrent.ScheduledThreadPoolExecutor;
69 import java.util.concurrent.TimeUnit;
70 import java.util.concurrent.TimeoutException;
71 
72 /**
73  * Orchestrator that runs the logic retrieved on a list of outcomes and signals.
74  *
75  * <p>Class takes in an executor on which it runs the OutcomeSelection logic
76  */
77 // TODO(b/269798827): Enable for R.
78 @RequiresApi(Build.VERSION_CODES.S)
79 public class OutcomeSelectionRunner {
80     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
81     @VisibleForTesting static final String AD_SELECTION_FROM_OUTCOMES_ERROR_PATTERN = "%s: %s";
82 
83     @VisibleForTesting
84     static final String ERROR_AD_SELECTION_FROM_OUTCOMES_FAILURE =
85             "Encountered failure during Ad Selection";
86 
87     @VisibleForTesting
88     static final String SELECTED_OUTCOME_MUST_BE_ONE_OF_THE_INPUTS =
89             "Outcome selection must return a valid ad selection id";
90 
91     @VisibleForTesting
92     static final String AD_SELECTION_TIMED_OUT = "Ad selection exceeded allowed time limit";
93 
94     @NonNull private final AdSelectionEntryDao mAdSelectionEntryDao;
95     @NonNull private final ListeningExecutorService mBackgroundExecutorService;
96     @NonNull private final ListeningExecutorService mLightweightExecutorService;
97     @NonNull private final ScheduledThreadPoolExecutor mScheduledExecutor;
98     @NonNull private final AdServicesHttpsClient mAdServicesHttpsClient;
99     @NonNull private final AdServicesLogger mAdServicesLogger;
100     @NonNull private final Context mContext;
101     @NonNull private final Flags mFlags;
102 
103     @NonNull private final AdOutcomeSelector mAdOutcomeSelector;
104     @NonNull private final AdSelectionServiceFilter mAdSelectionServiceFilter;
105     private final int mCallerUid;
106     @NonNull private final PrebuiltLogicGenerator mPrebuiltLogicGenerator;
107 
108     /**
109      * @param adSelectionEntryDao DAO to access ad selection storage
110      * @param backgroundExecutorService executor for longer running tasks (ex. network calls)
111      * @param lightweightExecutorService executor for running short tasks
112      * @param scheduledExecutor executor for tasks to be run with a delay or timed executions
113      * @param adServicesHttpsClient HTTPS client to use when fetch JS logics
114      * @param adServicesLogger logger for logging calls to PPAPI
115      * @param context service context
116      * @param flags for accessing feature flags
117      * @param adSelectionServiceFilter to validate the request
118      */
OutcomeSelectionRunner( @onNull final AdSelectionEntryDao adSelectionEntryDao, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @NonNull final AdServicesHttpsClient adServicesHttpsClient, @NonNull final AdServicesLogger adServicesLogger, @NonNull final DevContext devContext, @NonNull final Context context, @NonNull final Flags flags, @NonNull final AdSelectionServiceFilter adSelectionServiceFilter, @NonNull final AdCounterKeyCopier adCounterKeyCopier, final int callerUid)119     public OutcomeSelectionRunner(
120             @NonNull final AdSelectionEntryDao adSelectionEntryDao,
121             @NonNull final ExecutorService backgroundExecutorService,
122             @NonNull final ExecutorService lightweightExecutorService,
123             @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
124             @NonNull final AdServicesHttpsClient adServicesHttpsClient,
125             @NonNull final AdServicesLogger adServicesLogger,
126             @NonNull final DevContext devContext,
127             @NonNull final Context context,
128             @NonNull final Flags flags,
129             @NonNull final AdSelectionServiceFilter adSelectionServiceFilter,
130             @NonNull final AdCounterKeyCopier adCounterKeyCopier,
131             final int callerUid) {
132         Objects.requireNonNull(adSelectionEntryDao);
133         Objects.requireNonNull(backgroundExecutorService);
134         Objects.requireNonNull(lightweightExecutorService);
135         Objects.requireNonNull(scheduledExecutor);
136         Objects.requireNonNull(adServicesHttpsClient);
137         Objects.requireNonNull(adServicesLogger);
138         Objects.requireNonNull(devContext);
139         Objects.requireNonNull(context);
140         Objects.requireNonNull(flags);
141         Objects.requireNonNull(adCounterKeyCopier);
142 
143         mAdSelectionEntryDao = adSelectionEntryDao;
144         mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService);
145         mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService);
146         mScheduledExecutor = scheduledExecutor;
147         mAdServicesHttpsClient = adServicesHttpsClient;
148         mAdServicesLogger = adServicesLogger;
149         mContext = context;
150         mFlags = flags;
151 
152         mAdOutcomeSelector =
153                 new AdOutcomeSelectorImpl(
154                         new AdSelectionScriptEngine(
155                                 mContext,
156                                 flags::getEnforceIsolateMaxHeapSize,
157                                 flags::getIsolateMaxHeapSizeBytes,
158                                 adCounterKeyCopier),
159                         mLightweightExecutorService,
160                         mBackgroundExecutorService,
161                         mScheduledExecutor,
162                         mAdServicesHttpsClient,
163                         new AdSelectionDevOverridesHelper(devContext, adSelectionEntryDao),
164                         mFlags);
165         mAdSelectionServiceFilter = adSelectionServiceFilter;
166         mCallerUid = callerUid;
167         mPrebuiltLogicGenerator = new PrebuiltLogicGenerator(mFlags);
168     }
169 
170     @VisibleForTesting
OutcomeSelectionRunner( int callerUid, @NonNull final AdOutcomeSelector adOutcomeSelector, @NonNull final AdSelectionEntryDao adSelectionEntryDao, @NonNull final ExecutorService backgroundExecutorService, @NonNull final ExecutorService lightweightExecutorService, @NonNull final ScheduledThreadPoolExecutor scheduledExecutor, @NonNull final AdServicesLogger adServicesLogger, @NonNull final Context context, @NonNull final Flags flags, @NonNull final AdSelectionServiceFilter adSelectionServiceFilter)171     public OutcomeSelectionRunner(
172             int callerUid,
173             @NonNull final AdOutcomeSelector adOutcomeSelector,
174             @NonNull final AdSelectionEntryDao adSelectionEntryDao,
175             @NonNull final ExecutorService backgroundExecutorService,
176             @NonNull final ExecutorService lightweightExecutorService,
177             @NonNull final ScheduledThreadPoolExecutor scheduledExecutor,
178             @NonNull final AdServicesLogger adServicesLogger,
179             @NonNull final Context context,
180             @NonNull final Flags flags,
181             @NonNull final AdSelectionServiceFilter adSelectionServiceFilter) {
182         Objects.requireNonNull(adOutcomeSelector);
183         Objects.requireNonNull(adSelectionEntryDao);
184         Objects.requireNonNull(backgroundExecutorService);
185         Objects.requireNonNull(lightweightExecutorService);
186         Objects.requireNonNull(scheduledExecutor);
187         Objects.requireNonNull(adServicesLogger);
188         Objects.requireNonNull(context);
189         Objects.requireNonNull(flags);
190         Objects.requireNonNull(adSelectionServiceFilter);
191 
192         mAdSelectionEntryDao = adSelectionEntryDao;
193         mBackgroundExecutorService = MoreExecutors.listeningDecorator(backgroundExecutorService);
194         mLightweightExecutorService = MoreExecutors.listeningDecorator(lightweightExecutorService);
195         mScheduledExecutor = scheduledExecutor;
196         mAdServicesHttpsClient =
197                 new AdServicesHttpsClient(
198                         AdServicesExecutors.getBlockingExecutor(),
199                         CacheProviderFactory.create(context, flags));
200         mAdServicesLogger = adServicesLogger;
201         mContext = context;
202         mFlags = flags;
203 
204         mAdOutcomeSelector = adOutcomeSelector;
205         mAdSelectionServiceFilter = adSelectionServiceFilter;
206         mCallerUid = callerUid;
207         mPrebuiltLogicGenerator = new PrebuiltLogicGenerator(mFlags);
208     }
209 
210     /**
211      * Runs outcome selection logic on given list of outcomes and signals.
212      *
213      * @param inputParams includes list of outcomes, selection signals and URI to download the logic
214      * @param callback is used to notify the results to the caller
215      */
runOutcomeSelection( @onNull AdSelectionFromOutcomesInput inputParams, @NonNull AdSelectionCallback callback)216     public void runOutcomeSelection(
217             @NonNull AdSelectionFromOutcomesInput inputParams,
218             @NonNull AdSelectionCallback callback) {
219         Objects.requireNonNull(inputParams);
220         Objects.requireNonNull(callback);
221         AdSelectionFromOutcomesConfig adSelectionFromOutcomesConfig =
222                 inputParams.getAdSelectionFromOutcomesConfig();
223         try {
224             ListenableFuture<Void> filterAndValidateRequestFuture =
225                     Futures.submit(
226                             () -> {
227                                 try {
228                                     Trace.beginSection(Tracing.VALIDATE_REQUEST);
229                                     sLogger.v("Starting filtering and validation.");
230                                     mAdSelectionServiceFilter.filterRequest(
231                                             adSelectionFromOutcomesConfig.getSeller(),
232                                             inputParams.getCallerPackageName(),
233                                             mFlags
234                                                     .getEnforceForegroundStatusForFledgeRunAdSelection(),
235                                             true,
236                                             mCallerUid,
237                                             AdServicesStatsLog
238                                                     .AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
239                                             Throttler.ApiKey.FLEDGE_API_SELECT_ADS);
240                                     validateAdSelectionFromOutcomesConfig(inputParams);
241                                 } finally {
242                                     sLogger.v("Completed filtering and validation.");
243                                     Trace.endSection();
244                                 }
245                             },
246                             mLightweightExecutorService);
247 
248             ListenableFuture<AdSelectionOutcome> adSelectionOutcomeFuture =
249                     FluentFuture.from(filterAndValidateRequestFuture)
250                             .transformAsync(
251                                     ignoredVoid ->
252                                             orchestrateOutcomeSelection(
253                                                     inputParams.getAdSelectionFromOutcomesConfig(),
254                                                     inputParams.getCallerPackageName()),
255                                     mLightweightExecutorService);
256 
257             Futures.addCallback(
258                     adSelectionOutcomeFuture,
259                     new FutureCallback<AdSelectionOutcome>() {
260                         @Override
261                         public void onSuccess(AdSelectionOutcome result) {
262                             notifySuccessToCaller(result, callback);
263                         }
264 
265                         @Override
266                         public void onFailure(Throwable t) {
267                             if (t instanceof FilterException
268                                     && t.getCause()
269                                             instanceof ConsentManager.RevokedConsentException) {
270                                 // Skip logging if a FilterException occurs.
271                                 // AdSelectionServiceFilter ensures the failing assertion is logged
272                                 // internally.
273 
274                                 // Fail Silently by notifying success to caller
275                                 notifyEmptySuccessToCaller(callback);
276                             } else {
277                                 if (t.getCause() instanceof AdServicesException) {
278                                     notifyFailureToCaller(t.getCause(), callback);
279                                 } else {
280                                     notifyFailureToCaller(t, callback);
281                                 }
282                             }
283                         }
284                     },
285                     mLightweightExecutorService);
286 
287         } catch (Throwable t) {
288             sLogger.v("runOutcomeSelection fails fast with exception %s.", t.toString());
289             notifyFailureToCaller(t, callback);
290         }
291     }
292 
orchestrateOutcomeSelection( @onNull AdSelectionFromOutcomesConfig config, @NonNull String callerPackageName)293     private ListenableFuture<AdSelectionOutcome> orchestrateOutcomeSelection(
294             @NonNull AdSelectionFromOutcomesConfig config, @NonNull String callerPackageName) {
295         FluentFuture<List<AdSelectionIdWithBidAndRenderUri>> outcomeIdBidPairsFuture =
296                 FluentFuture.from(
297                         retrieveAdSelectionIdWithBidList(
298                                 config.getAdSelectionIds(), callerPackageName));
299 
300         FluentFuture<Long> selectedAdSelectionIdFuture =
301                 outcomeIdBidPairsFuture.transformAsync(
302                         outcomeIdBids ->
303                                 mAdOutcomeSelector.runAdOutcomeSelector(outcomeIdBids, config),
304                         mLightweightExecutorService);
305 
306         return selectedAdSelectionIdFuture
307                 .transformAsync(
308                         selectedId ->
309                                 (selectedId != null)
310                                         ? convertAdSelectionIdToAdSelectionOutcome(
311                                                 outcomeIdBidPairsFuture, selectedId)
312                                         : Futures.immediateFuture(null),
313                         mLightweightExecutorService)
314                 .withTimeout(
315                         mFlags.getAdSelectionFromOutcomesOverallTimeoutMs(),
316                         TimeUnit.MILLISECONDS,
317                         mScheduledExecutor)
318                 .catching(
319                         TimeoutException.class,
320                         this::handleTimeoutError,
321                         mLightweightExecutorService);
322     }
323 
324     @Nullable
handleTimeoutError(TimeoutException e)325     private AdSelectionOutcome handleTimeoutError(TimeoutException e) {
326         sLogger.e(e, "Ad Selection exceeded time limit");
327         throw new UncheckedTimeoutException(AD_SELECTION_TIMED_OUT);
328     }
329 
notifySuccessToCaller(AdSelectionOutcome result, AdSelectionCallback callback)330     private void notifySuccessToCaller(AdSelectionOutcome result, AdSelectionCallback callback) {
331         int resultCode = AdServicesStatusUtils.STATUS_UNSET;
332         try {
333             // Note: Success is logged before the callback to ensure deterministic testing.
334             mAdServicesLogger.logFledgeApiCallStats(
335                     AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN,
336                     AdServicesStatusUtils.STATUS_SUCCESS,
337                     0);
338             if (result == null) {
339                 callback.onSuccess(null);
340             } else {
341                 callback.onSuccess(
342                         new AdSelectionResponse.Builder()
343                                 .setAdSelectionId(result.getAdSelectionId())
344                                 .setRenderUri(result.getRenderUri())
345                                 .build());
346             }
347         } catch (RemoteException e) {
348             sLogger.e(e, "Encountered exception during notifying AdSelectionCallback");
349         } finally {
350             sLogger.v("Ad Selection from outcomes completed and attempted notifying success");
351         }
352     }
353 
354     /** Sends a successful response to the caller that represents a silent failure. */
notifyEmptySuccessToCaller(@onNull AdSelectionCallback callback)355     private void notifyEmptySuccessToCaller(@NonNull AdSelectionCallback callback) {
356         try {
357             // TODO(b/259522822): Determine what is an appropriate empty response for revoked
358             //  consent for selectAdsFromOutcomes
359             callback.onSuccess(null);
360         } catch (RemoteException e) {
361             sLogger.e(e, "Encountered exception during notifying AdSelectionCallback");
362         } finally {
363             sLogger.v(
364                     "Ad Selection from outcomes completed, attempted notifying success for a"
365                             + " silent failure");
366         }
367     }
368 
369     /** Sends a failure notification to the caller */
notifyFailureToCaller(Throwable t, AdSelectionCallback callback)370     private void notifyFailureToCaller(Throwable t, AdSelectionCallback callback) {
371         try {
372             sLogger.e("Notify caller of error: " + t);
373             int resultCode = AdServicesLoggerUtil.getResultCodeFromException(t);
374 
375             // Skip logging if a FilterException occurs.
376             // AdSelectionServiceFilter ensures the failing assertion is logged internally.
377             // Note: Failure is logged before the callback to ensure deterministic testing.
378             if (!(t instanceof FilterException)) {
379                 mAdServicesLogger.logFledgeApiCallStats(
380                         AD_SERVICES_API_CALLED__API_NAME__API_NAME_UNKNOWN, resultCode, 0);
381             }
382 
383             FledgeErrorResponse selectionFailureResponse =
384                     new FledgeErrorResponse.Builder()
385                             .setErrorMessage(
386                                     String.format(
387                                             AD_SELECTION_FROM_OUTCOMES_ERROR_PATTERN,
388                                             ERROR_AD_SELECTION_FROM_OUTCOMES_FAILURE,
389                                             t.getMessage()))
390                             .setStatusCode(resultCode)
391                             .build();
392             sLogger.e(t, "Ad Selection failure: ");
393             callback.onFailure(selectionFailureResponse);
394         } catch (RemoteException e) {
395             sLogger.e(e, "Encountered exception during notifying AdSelectionCallback");
396         } finally {
397             sLogger.v("Ad Selection From Outcomes failed");
398         }
399     }
400 
401     /** Retrieves winner ad bids using ad selection ids of already run ad selections' outcomes. */
402     private ListenableFuture<List<AdSelectionIdWithBidAndRenderUri>>
retrieveAdSelectionIdWithBidList(List<Long> adOutcomeIds, String callerPackageName)403             retrieveAdSelectionIdWithBidList(List<Long> adOutcomeIds, String callerPackageName) {
404         List<AdSelectionIdWithBidAndRenderUri> adSelectionIdWithBidAndRenderUriList =
405                 new ArrayList<>();
406         return mBackgroundExecutorService.submit(
407                 () -> {
408                     mAdSelectionEntryDao
409                             .getAdSelectionEntities(adOutcomeIds, callerPackageName)
410                             .parallelStream()
411                             .forEach(
412                                     e ->
413                                             adSelectionIdWithBidAndRenderUriList.add(
414                                                     AdSelectionIdWithBidAndRenderUri.builder()
415                                                             .setAdSelectionId(e.getAdSelectionId())
416                                                             .setBid(e.getWinningAdBid())
417                                                             .setRenderUri(e.getWinningAdRenderUri())
418                                                             .build()));
419                     return adSelectionIdWithBidAndRenderUriList;
420                 });
421     }
422 
423     /** Retrieves winner ad bids using ad selection ids of already run ad selections' outcomes. */
convertAdSelectionIdToAdSelectionOutcome( FluentFuture<List<AdSelectionIdWithBidAndRenderUri>> adSelectionIdWithBidAndRenderUrisFuture, Long adSelectionId)424     private ListenableFuture<AdSelectionOutcome> convertAdSelectionIdToAdSelectionOutcome(
425             FluentFuture<List<AdSelectionIdWithBidAndRenderUri>>
426                     adSelectionIdWithBidAndRenderUrisFuture,
427             Long adSelectionId) {
428         return adSelectionIdWithBidAndRenderUrisFuture.transformAsync(
429                 idWithBidAndUris -> {
430                     sLogger.i(
431                             "Converting ad selection id: <%s> to AdSelectionOutcome.",
432                             adSelectionId);
433                     return idWithBidAndUris.stream()
434                             .filter(e -> Objects.equals(e.getAdSelectionId(), adSelectionId))
435                             .findFirst()
436                             .map(
437                                     e ->
438                                             Futures.immediateFuture(
439                                                     new AdSelectionOutcome.Builder()
440                                                             .setAdSelectionId(e.getAdSelectionId())
441                                                             .setRenderUri(e.getRenderUri())
442                                                             .build()))
443                             .orElse(
444                                     Futures.immediateFailedFuture(
445                                             new IllegalStateException(
446                                                     SELECTED_OUTCOME_MUST_BE_ONE_OF_THE_INPUTS)));
447                 },
448                 mLightweightExecutorService);
449     }
450     /**
451      * Validates the {@link AdSelectionFromOutcomesInput} from the request.
452      *
453      * @param inputParams the adSelectionConfig to be validated
454      * @throws IllegalArgumentException if the provided {@code adSelectionConfig} is not valid
455      */
456     private void validateAdSelectionFromOutcomesConfig(
457             @NonNull AdSelectionFromOutcomesInput inputParams) throws IllegalArgumentException {
458         Objects.requireNonNull(inputParams);
459 
460         AdSelectionFromOutcomesConfigValidator validator =
461                 new AdSelectionFromOutcomesConfigValidator(
462                         mAdSelectionEntryDao,
463                         inputParams.getCallerPackageName(),
464                         mPrebuiltLogicGenerator);
465         validator.validate(inputParams.getAdSelectionFromOutcomesConfig());
466     }
467 
468 }
469