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