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.js.JSScriptArgument.arrayArg; 20 import static com.android.adservices.service.js.JSScriptArgument.jsonArg; 21 import static com.android.adservices.service.js.JSScriptArgument.recordArg; 22 import static com.android.adservices.service.js.JSScriptArgument.stringArg; 23 import static com.android.adservices.service.js.JSScriptArgument.stringArrayArg; 24 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.ENTRY_POINT_FUNC_NAME; 25 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.JS_EXECUTION_RESULT_INVALID; 26 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.JS_EXECUTION_STATUS_UNSUCCESSFUL; 27 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.JS_SCRIPT_STATUS_SUCCESS; 28 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.RESULTS_FIELD_NAME; 29 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.SCRIPT_ARGUMENT_NAME_IGNORED; 30 import static com.android.adservices.service.js.JSScriptEngineCommonConstants.STATUS_FIELD_NAME; 31 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.JS_RUN_STATUS_JS_REFERENCE_ERROR; 32 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.JS_RUN_STATUS_OTHER_FAILURE; 33 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.JS_RUN_STATUS_OUTPUT_SEMANTIC_ERROR; 34 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.JS_RUN_STATUS_OUTPUT_SYNTAX_ERROR; 35 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.JS_RUN_STATUS_SUCCESS; 36 37 import static com.google.common.util.concurrent.Futures.transform; 38 39 import android.adservices.adselection.AdSelectionConfig; 40 import android.adservices.adselection.AdWithBid; 41 import android.adservices.common.AdData; 42 import android.adservices.common.AdSelectionSignals; 43 import android.annotation.NonNull; 44 import android.net.Uri; 45 import android.webkit.URLUtil; 46 47 import com.android.adservices.LoggerFactory; 48 import com.android.adservices.data.adselection.CustomAudienceSignals; 49 import com.android.adservices.data.adselection.datahandlers.AdSelectionResultBidAndUri; 50 import com.android.adservices.data.common.DBAdData; 51 import com.android.adservices.data.customaudience.DBCustomAudience; 52 import com.android.adservices.service.common.RetryStrategy; 53 import com.android.adservices.service.exception.JSExecutionException; 54 import com.android.adservices.service.js.IsolateSettings; 55 import com.android.adservices.service.js.JSScriptArgument; 56 import com.android.adservices.service.js.JSScriptEngine; 57 import com.android.adservices.service.js.JSScriptEngineCommonConstants; 58 import com.android.adservices.service.profiling.Tracing; 59 import com.android.adservices.service.stats.AdSelectionExecutionLogger; 60 import com.android.adservices.service.stats.RunAdBiddingPerCAExecutionLogger; 61 import com.android.adservices.service.stats.SelectAdsFromOutcomesExecutionLogger; 62 import com.android.internal.annotations.VisibleForTesting; 63 64 import com.google.common.base.Strings; 65 import com.google.common.collect.ImmutableList; 66 import com.google.common.util.concurrent.FluentFuture; 67 import com.google.common.util.concurrent.ListenableFuture; 68 import com.google.common.util.concurrent.MoreExecutors; 69 70 import org.json.JSONArray; 71 import org.json.JSONException; 72 import org.json.JSONObject; 73 74 import java.util.List; 75 import java.util.Objects; 76 import java.util.concurrent.Executor; 77 import java.util.function.Function; 78 import java.util.function.Supplier; 79 import java.util.stream.Collectors; 80 81 /** 82 * Utility class to execute an auction script. Current implementation is thread safe but relies on a 83 * singleton JS execution environment and will serialize calls done either using the same or 84 * different instances of {@link AdSelectionScriptEngine}. This will change once we will use the new 85 * WebView API. 86 * 87 * <p>This class is thread safe but, for performance reasons, it is suggested to use one instance 88 * per thread. See the threading comments for {@link JSScriptEngine}. 89 */ 90 public class AdSelectionScriptEngine { 91 private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger(); 92 93 public static final String FUNCTION_NAMES_ARG_NAME = "__rb_functionNames"; 94 // This is a local variable and doesn't need any prefix. 95 public static final String CUSTOM_AUDIENCE_ARG_NAME = "__rb_custom_audience"; 96 public static final String AD_VAR_NAME = "ad"; 97 public static final String ADS_ARG_NAME = "__rb_ads"; 98 public static final String AUCTION_SIGNALS_ARG_NAME = "__rb_auction_signals"; 99 public static final String PER_BUYER_SIGNALS_ARG_NAME = "__rb_per_buyer_signals"; 100 public static final String TRUSTED_BIDDING_SIGNALS_ARG_NAME = "__rb_trusted_bidding_signals"; 101 public static final String CONTEXTUAL_SIGNALS_ARG_NAME = "__rb_contextual_signals"; 102 public static final String CUSTOM_AUDIENCE_BIDDING_SIGNALS_ARG_NAME = 103 "__rb_custom_audience_bidding_signals"; 104 public static final String CUSTOM_AUDIENCE_SCORING_SIGNALS_ARG_NAME = 105 "__rb_custom_audience_scoring_signals"; 106 public static final String AUCTION_CONFIG_ARG_NAME = "__rb_auction_config"; 107 public static final String SELLER_SIGNALS_ARG_NAME = "__rb_seller_signals"; 108 public static final String TRUSTED_SCORING_SIGNALS_ARG_NAME = "__rb_trusted_scoring_signals"; 109 public static final String GENERATE_BID_FUNCTION_NAME = "generateBid"; 110 public static final String SCORE_AD_FUNCTION_NAME = "scoreAd"; 111 112 public static final String USER_SIGNALS_ARG_NAME = "__rb_user_signals"; 113 public static final String AD_SCORE_FIELD_NAME = "score"; 114 public static final String DEBUG_REPORTING_WIN_URI_FIELD_NAME = "debug_reporting_win_uri"; 115 public static final String DEBUG_REPORTING_LOSS_URI_FIELD_NAME = "debug_reporting_loss_uri"; 116 public static final String DEBUG_REPORTING_SELLER_REJECT_REASON_FIELD_NAME = "rejectReason"; 117 public static final String AD_COST_FIELD_NAME = "adCost"; 118 public static final int NUM_BITS_STOCHASTIC_ROUNDING = 8; 119 120 /** 121 * Template for the iterative invocation function. The two tokens to expand are the list of 122 * parameters and the invocation of the actual per-ad function. 123 */ 124 public static final String AD_SELECTION_ITERATIVE_PROCESSING_JS = 125 "function " 126 + ENTRY_POINT_FUNC_NAME 127 + "(%s) {\n" 128 + " let status = 0;\n" 129 + " const results = []; \n" 130 + " for (const " 131 + AD_VAR_NAME 132 + " of " 133 + ADS_ARG_NAME 134 + ") {\n" 135 + " //Short circuit the processing of all ads if there was any failure.\n" 136 + " const script_result = %s;\n" 137 + " if (script_result === Object(script_result) && \n" 138 + " 'status' in script_result) {\n" 139 + " status = script_result.status;\n" 140 + " } else {\n" 141 + " // invalid script\n" 142 + " status = -1;\n" 143 + " } \n" 144 + " if (status != 0) break;\n" 145 + " script_result.debug_reporting_win_uri = " 146 + DebugReportingEnabledScriptStrategy.WIN_URI_GLOBAL_VARIABLE 147 + ";\n" 148 + " script_result.debug_reporting_loss_uri = " 149 + DebugReportingEnabledScriptStrategy.LOSS_URI_GLOBAL_VARIABLE 150 + ";\n" 151 + DebugReportingEnabledScriptStrategy.RESET_SCRIPT 152 + " results.push(script_result);\n" 153 + " }\n" 154 + " return {'status': status, 'results': results};\n" 155 + "};"; 156 157 /** 158 * Template for the batch invocation function. The two tokens to expand are the list of 159 * parameters and the invocation of the actual per-ad function. 160 */ 161 public static final String AD_SELECTION_BATCH_PROCESSING_JS = 162 "function " 163 + ENTRY_POINT_FUNC_NAME 164 + "(%s) {\n" 165 + " let status = 0;\n" 166 + " const results = []; \n" 167 + " const script_result = %s;\n" 168 + " if (script_result === Object(script_result) && \n" 169 + " 'status' in script_result && \n" 170 + " 'result' in script_result) {\n" 171 + " status = script_result.status;\n" 172 + " results.push(script_result.result)\n" 173 + " } else {\n" 174 + " // invalid script\n" 175 + " status = -1;\n" 176 + " }\n" 177 + " return {'status': status, 'results': results};\n" 178 + "};"; 179 180 public static final String AD_SELECTION_GENERATE_BID_JS_V3 = 181 "function " 182 + ENTRY_POINT_FUNC_NAME 183 + "(%s) {\n" 184 + " let status = 0;\n" 185 + " let results = null;\n" 186 + " const script_result = %s;\n" 187 + " if (script_result === Object(script_result) &&\n" 188 + " 'ad' in script_result &&\n" 189 + " 'bid' in script_result &&\n" 190 + " 'render' in script_result) {\n" 191 + " results = [{" 192 + " 'ad': script_result.ad,\n" 193 + " 'bid': script_result.bid,\n" 194 + " 'adCost': script_result.adCost,\n" 195 + " 'debug_reporting_win_uri': " 196 + DebugReportingEnabledScriptStrategy.WIN_URI_GLOBAL_VARIABLE 197 + ",\n" 198 + " 'debug_reporting_loss_uri': " 199 + DebugReportingEnabledScriptStrategy.LOSS_URI_GLOBAL_VARIABLE 200 + ",\n" 201 + " }];\n" 202 + " } else {\n" 203 + " // invalid script\n" 204 + " status = -1;\n" 205 + " }\n" 206 + " return {'status': status, 'results': results};\n" 207 + "};"; 208 209 public static final String CHECK_FUNCTIONS_EXIST_JS = 210 "function " 211 + ENTRY_POINT_FUNC_NAME 212 + "(names) {\n" 213 + " for (const name of names) {\n" 214 + " try {\n" 215 + " if (typeof eval(name) != 'function') return false;\n" 216 + " } catch(e) {\n" 217 + " if (e instanceof ReferenceError) return false;\n" 218 + " }\n" 219 + " }\n" 220 + " return true;\n" 221 + "}"; 222 public static final String GET_FUNCTION_ARGUMENT_COUNT = 223 "function " 224 + ENTRY_POINT_FUNC_NAME 225 + "(names) {\n" 226 + " for (const name of names) {\n" 227 + " try {\n" 228 + " if (typeof eval(name) != 'function') return -1;\n" 229 + " } catch(e) {\n" 230 + " if (e instanceof ReferenceError) return -1;\n" 231 + " }\n" 232 + " if (typeof eval(name) === 'function') return eval(name).length;\n" 233 + " }\n" 234 + " return -1;\n" 235 + "}"; 236 237 private static final String ARG_PASSING_SEPARATOR = ", "; 238 private final AdWithBidArgumentUtil mAdWithBidArgumentUtil; 239 private final AdDataArgumentUtil mAdDataArgumentUtil; 240 private final DebugReportingScriptStrategy mDebugReportingScript; 241 private final boolean mCpcBillingEnabled; 242 private final JSScriptEngine mJsEngine; 243 // Used for the Futures.transform calls to compose futures. 244 private final Executor mExecutor = MoreExecutors.directExecutor(); 245 private final Supplier<Boolean> mEnforceMaxHeapSizeFeatureSupplier; 246 private final Supplier<Long> mMaxHeapSizeBytesSupplier; 247 private final RetryStrategy mRetryStrategy; 248 private final Supplier<Boolean> mIsolateConsoleMessageInLogsEnabled; 249 AdSelectionScriptEngine( Supplier<Boolean> enforceMaxHeapSizeFeatureSupplier, Supplier<Long> maxHeapSizeBytesSupplier, AdCounterKeyCopier adCounterKeyCopier, DebugReportingScriptStrategy debugReportingScript, boolean cpcBillingEnabled, RetryStrategy retryStrategy, Supplier<Boolean> isolateConsoleMessageInLogsEnabled)250 public AdSelectionScriptEngine( 251 Supplier<Boolean> enforceMaxHeapSizeFeatureSupplier, 252 Supplier<Long> maxHeapSizeBytesSupplier, 253 AdCounterKeyCopier adCounterKeyCopier, 254 DebugReportingScriptStrategy debugReportingScript, 255 boolean cpcBillingEnabled, 256 RetryStrategy retryStrategy, 257 Supplier<Boolean> isolateConsoleMessageInLogsEnabled) { 258 mJsEngine = JSScriptEngine.getInstance(sLogger); 259 mAdDataArgumentUtil = new AdDataArgumentUtil(adCounterKeyCopier); 260 mAdWithBidArgumentUtil = new AdWithBidArgumentUtil(mAdDataArgumentUtil); 261 mDebugReportingScript = debugReportingScript; 262 mCpcBillingEnabled = cpcBillingEnabled; 263 mEnforceMaxHeapSizeFeatureSupplier = enforceMaxHeapSizeFeatureSupplier; 264 mMaxHeapSizeBytesSupplier = maxHeapSizeBytesSupplier; 265 mRetryStrategy = retryStrategy; 266 mIsolateConsoleMessageInLogsEnabled = isolateConsoleMessageInLogsEnabled; 267 } 268 269 /** 270 * @return The result of invoking the {@code generateBid} function in the given {@code 271 * generateBidJS} JS script for the list of {@code ads} and signals provided. Will return an 272 * empty list if the script fails for any reason. 273 * @throws JSONException If any of the signals is not a valid JSON object. 274 */ generateBids( @onNull String generateBidJS, @NonNull List<AdData> ads, @NonNull AdSelectionSignals auctionSignals, @NonNull AdSelectionSignals perBuyerSignals, @NonNull AdSelectionSignals trustedBiddingSignals, @NonNull AdSelectionSignals contextualSignals, @NonNull CustomAudienceSignals customAudienceSignals, @NonNull RunAdBiddingPerCAExecutionLogger runAdBiddingPerCAExecutionLogger)275 public ListenableFuture<List<GenerateBidResult>> generateBids( 276 @NonNull String generateBidJS, 277 @NonNull List<AdData> ads, 278 @NonNull AdSelectionSignals auctionSignals, 279 @NonNull AdSelectionSignals perBuyerSignals, 280 @NonNull AdSelectionSignals trustedBiddingSignals, 281 @NonNull AdSelectionSignals contextualSignals, 282 @NonNull CustomAudienceSignals customAudienceSignals, 283 @NonNull RunAdBiddingPerCAExecutionLogger runAdBiddingPerCAExecutionLogger) 284 throws JSONException { 285 Objects.requireNonNull(generateBidJS); 286 Objects.requireNonNull(ads); 287 Objects.requireNonNull(auctionSignals); 288 Objects.requireNonNull(perBuyerSignals); 289 Objects.requireNonNull(trustedBiddingSignals); 290 Objects.requireNonNull(contextualSignals); 291 Objects.requireNonNull(customAudienceSignals); 292 Objects.requireNonNull(runAdBiddingPerCAExecutionLogger); 293 int traceCookie = Tracing.beginAsyncSection(Tracing.GENERATE_BIDS); 294 295 ImmutableList<JSScriptArgument> signals = 296 ImmutableList.<JSScriptArgument>builder() 297 .add(jsonArg(AUCTION_SIGNALS_ARG_NAME, auctionSignals.toString())) 298 .add(jsonArg(PER_BUYER_SIGNALS_ARG_NAME, perBuyerSignals.toString())) 299 .add( 300 jsonArg( 301 TRUSTED_BIDDING_SIGNALS_ARG_NAME, 302 trustedBiddingSignals.toString())) 303 .add(jsonArg(CONTEXTUAL_SIGNALS_ARG_NAME, contextualSignals.toString())) 304 .add( 305 CustomAudienceBiddingSignalsArgumentUtil.asScriptArgument( 306 CUSTOM_AUDIENCE_BIDDING_SIGNALS_ARG_NAME, 307 customAudienceSignals)) 308 .build(); 309 310 ImmutableList.Builder<JSScriptArgument> adDataArguments = new ImmutableList.Builder<>(); 311 for (AdData currAd : ads) { 312 // Ads are going to be in an array their individual name is ignored. 313 adDataArguments.add(mAdDataArgumentUtil.asScriptArgument("ignored", currAd)); 314 } 315 runAdBiddingPerCAExecutionLogger.startGenerateBids(); 316 return FluentFuture.from( 317 transform( 318 runAuctionScriptIterative( 319 generateBidJS, 320 adDataArguments.build(), 321 signals, 322 this::callGenerateBid), 323 result -> { 324 List<GenerateBidResult> bidsResults = 325 handleGenerateBidsOutput( 326 result, runAdBiddingPerCAExecutionLogger); 327 runAdBiddingPerCAExecutionLogger.endGenerateBids(); 328 Tracing.endAsyncSection(Tracing.GENERATE_BIDS, traceCookie); 329 return bidsResults; 330 }, 331 mExecutor)) 332 .catchingAsync( 333 JSExecutionException.class, 334 e -> { 335 if (e.getMessage().contains("Uncaught ReferenceError:")) { 336 runAdBiddingPerCAExecutionLogger.setGenerateBidJsScriptResultCode( 337 JS_RUN_STATUS_JS_REFERENCE_ERROR); 338 } else { 339 runAdBiddingPerCAExecutionLogger.setGenerateBidJsScriptResultCode( 340 JS_RUN_STATUS_OTHER_FAILURE); 341 } 342 Tracing.endAsyncSection(Tracing.GENERATE_BIDS, traceCookie); 343 sLogger.e( 344 e, 345 "Encountered exception when generating bids, attempting to run" 346 + " backward compatible JS"); 347 return handleBackwardIncompatibilityScenario( 348 generateBidJS, 349 signals, 350 adDataArguments.build(), 351 runAdBiddingPerCAExecutionLogger, 352 e); 353 }, 354 mExecutor); 355 } 356 357 /** 358 * @return The result of invoking the {@code generateBidV3} function in the given {@code 359 * generateBidJS} JS script for the args provided. Will return an empty list if the script 360 * fails for any reason. 361 * @throws JSONException If any of the signals is not a valid JSON object. 362 */ 363 @NonNull generateBidsV3( @onNull String generateBidJS, @NonNull DBCustomAudience customAudience, @NonNull AdSelectionSignals auctionSignals, @NonNull AdSelectionSignals perBuyerSignals, @NonNull AdSelectionSignals trustedBiddingSignals, @NonNull AdSelectionSignals contextualSignals, @NonNull RunAdBiddingPerCAExecutionLogger runAdBiddingPerCAExecutionLogger)364 public ListenableFuture<List<GenerateBidResult>> generateBidsV3( 365 @NonNull String generateBidJS, 366 @NonNull DBCustomAudience customAudience, 367 @NonNull AdSelectionSignals auctionSignals, 368 @NonNull AdSelectionSignals perBuyerSignals, 369 @NonNull AdSelectionSignals trustedBiddingSignals, 370 @NonNull AdSelectionSignals contextualSignals, 371 @NonNull RunAdBiddingPerCAExecutionLogger runAdBiddingPerCAExecutionLogger) 372 throws JSONException { 373 Objects.requireNonNull(generateBidJS); 374 Objects.requireNonNull(customAudience); 375 Objects.requireNonNull(auctionSignals); 376 Objects.requireNonNull(perBuyerSignals); 377 Objects.requireNonNull(trustedBiddingSignals); 378 Objects.requireNonNull(contextualSignals); 379 Objects.requireNonNull(runAdBiddingPerCAExecutionLogger); 380 int traceCookie = Tracing.beginAsyncSection(Tracing.GENERATE_BIDS); 381 382 ImmutableList<JSScriptArgument> signals = 383 ImmutableList.<JSScriptArgument>builder() 384 .add(translateCustomAudience(customAudience)) 385 .add(jsonArg(AUCTION_SIGNALS_ARG_NAME, auctionSignals)) 386 .add(jsonArg(PER_BUYER_SIGNALS_ARG_NAME, perBuyerSignals)) 387 .add(jsonArg(TRUSTED_BIDDING_SIGNALS_ARG_NAME, trustedBiddingSignals)) 388 .add(jsonArg(CONTEXTUAL_SIGNALS_ARG_NAME, contextualSignals)) 389 .build(); 390 runAdBiddingPerCAExecutionLogger.startGenerateBids(); 391 return FluentFuture.from( 392 transform( 393 runAuctionScriptGenerateBidV3( 394 generateBidJS, signals, this::callGenerateBidV3), 395 result -> { 396 List<GenerateBidResult> bidResults = 397 handleGenerateBidsOutput( 398 result, runAdBiddingPerCAExecutionLogger); 399 runAdBiddingPerCAExecutionLogger.endGenerateBids(); 400 Tracing.endAsyncSection(Tracing.GENERATE_BIDS, traceCookie); 401 return bidResults; 402 }, 403 mExecutor)); 404 } 405 406 /** 407 * @return The scored ads for this custom audiences given the list of Ads with associated bid 408 * and the set of signals. Will return an empty list if the script fails for any reason. 409 * @throws JSONException If any of the data is not a valid JSON object. 410 */ 411 public ListenableFuture<List<ScoreAdResult>> scoreAds( 412 @NonNull String scoreAdJS, 413 @NonNull List<AdWithBid> adsWithBid, 414 @NonNull AdSelectionConfig adSelectionConfig, 415 @NonNull AdSelectionSignals sellerSignals, 416 @NonNull AdSelectionSignals trustedScoringSignals, 417 @NonNull AdSelectionSignals contextualSignals, 418 @NonNull List<CustomAudienceSignals> customAudienceSignalsList, 419 @NonNull AdSelectionExecutionLogger adSelectionExecutionLogger) 420 throws JSONException { 421 Objects.requireNonNull(scoreAdJS); 422 Objects.requireNonNull(adsWithBid); 423 Objects.requireNonNull(adSelectionConfig); 424 Objects.requireNonNull(sellerSignals); 425 Objects.requireNonNull(trustedScoringSignals); 426 Objects.requireNonNull(contextualSignals); 427 Objects.requireNonNull(customAudienceSignalsList); 428 Objects.requireNonNull(adSelectionExecutionLogger); 429 ImmutableList<JSScriptArgument> args = 430 ImmutableList.<JSScriptArgument>builder() 431 .add( 432 AdSelectionConfigArgumentUtil.asScriptArgument( 433 adSelectionConfig, AUCTION_CONFIG_ARG_NAME)) 434 .add(jsonArg(SELLER_SIGNALS_ARG_NAME, sellerSignals.toString())) 435 .add( 436 jsonArg( 437 TRUSTED_SCORING_SIGNALS_ARG_NAME, 438 trustedScoringSignals.toString())) 439 .add(jsonArg(CONTEXTUAL_SIGNALS_ARG_NAME, contextualSignals.toString())) 440 .add( 441 CustomAudienceScoringSignalsArgumentUtil.asScriptArgument( 442 CUSTOM_AUDIENCE_SCORING_SIGNALS_ARG_NAME, 443 customAudienceSignalsList)) 444 .build(); 445 446 ImmutableList.Builder<JSScriptArgument> adWithBidArguments = new ImmutableList.Builder<>(); 447 for (AdWithBid currAdWithBid : adsWithBid) { 448 // Ad with bids are going to be in an array their individual name is ignored. 449 adWithBidArguments.add( 450 mAdWithBidArgumentUtil.asScriptArgument( 451 SCRIPT_ARGUMENT_NAME_IGNORED, currAdWithBid)); 452 } 453 // Start scoreAds script execution process. 454 adSelectionExecutionLogger.startScoreAds(); 455 return FluentFuture.from( 456 runAuctionScriptIterative( 457 scoreAdJS, adWithBidArguments.build(), args, this::callScoreAd)) 458 .transform( 459 result -> handleScoreAdsOutput(result, adSelectionExecutionLogger), 460 mExecutor) 461 .catchingAsync( 462 JSExecutionException.class, 463 e -> { 464 if (e.getMessage().contains("Uncaught ReferenceError:")) { 465 adSelectionExecutionLogger.setScoreAdJsScriptResultCode( 466 JS_RUN_STATUS_JS_REFERENCE_ERROR); 467 } else { 468 adSelectionExecutionLogger.setScoreAdJsScriptResultCode( 469 JS_RUN_STATUS_OTHER_FAILURE); 470 } 471 throw e; 472 }, 473 mExecutor); 474 } 475 476 /** 477 * Runs selection logic on map of {@code long} ad selection id {@code double} bid 478 * 479 * @return either one of the ad selection ids passed in {@code adSelectionIdBidPairs} or {@code 480 * null} 481 * @throws JSONException if any input or the result is failed to parse 482 * @throws IllegalStateException If JS script fails to run or returns an illegal results (i.e. 483 * two ad selection ids or empty) 484 */ 485 public ListenableFuture<Long> selectOutcome( 486 @NonNull String selectionLogic, 487 @NonNull List<AdSelectionResultBidAndUri> adSelectionIdWithBidAndRenderUris, 488 @NonNull AdSelectionSignals selectionSignals, 489 @NonNull SelectAdsFromOutcomesExecutionLogger selectAdsFromOutcomesExecutionLogger) 490 throws JSONException, IllegalStateException { 491 Objects.requireNonNull(selectionLogic); 492 Objects.requireNonNull(adSelectionIdWithBidAndRenderUris); 493 Objects.requireNonNull(selectionSignals); 494 Objects.requireNonNull(selectAdsFromOutcomesExecutionLogger); 495 496 ImmutableList<JSScriptArgument> args = 497 ImmutableList.<JSScriptArgument>builder() 498 .add(jsonArg("selection_signals", selectionSignals.toString())) 499 .build(); 500 sLogger.v("Other args creates " + args); 501 502 ImmutableList.Builder<JSScriptArgument> adSelectionIdWithBidArguments = 503 new ImmutableList.Builder<>(); 504 for (AdSelectionResultBidAndUri curr : adSelectionIdWithBidAndRenderUris) { 505 // Ad with bids are going to be in an array their individual name is ignored. 506 adSelectionIdWithBidArguments.add( 507 SelectAdsFromOutcomesArgumentUtil.asScriptArgument( 508 SCRIPT_ARGUMENT_NAME_IGNORED, curr)); 509 } 510 ImmutableList<JSScriptArgument> advertArgs = adSelectionIdWithBidArguments.build(); 511 sLogger.v("Advert args created " + advertArgs); 512 513 // Start selectOutcome script execution process. 514 selectAdsFromOutcomesExecutionLogger.startExecutionScriptTimestamp(); 515 return transform( 516 runAuctionScriptBatch(selectionLogic, advertArgs, args, this::callSelectOutcome), 517 result -> handleSelectOutcomesOutput(result, selectAdsFromOutcomesExecutionLogger), 518 mExecutor); 519 } 520 521 /** 522 * Parses the output from the invocation of the {@code generateBid} JS function on a list of ads 523 * and convert it to a list of {@link GenerateBidResult} objects. The script output has been 524 * pre-parsed into an {@link AuctionScriptResult} object that will contain the script status 525 * code and the list of ads. The method will return an empty list of ads if the status code is 526 * not {@link JSScriptEngineCommonConstants#JS_SCRIPT_STATUS_SUCCESS} or if there has been any 527 * problem parsing the JS response. 528 */ 529 private List<GenerateBidResult> handleGenerateBidsOutput( 530 AuctionScriptResult batchBidResult, 531 RunAdBiddingPerCAExecutionLogger runAdBiddingPerCAExecutionLogger) { 532 ImmutableList.Builder<GenerateBidResult> results = ImmutableList.builder(); 533 if (batchBidResult.status != JS_SCRIPT_STATUS_SUCCESS) { 534 sLogger.v("Bid script failed, returning empty result."); 535 runAdBiddingPerCAExecutionLogger.setGenerateBidJsScriptResultCode( 536 JS_RUN_STATUS_OUTPUT_SYNTAX_ERROR); 537 return ImmutableList.of(); 538 } 539 540 try { 541 for (int i = 0; i < batchBidResult.results.length(); i++) { 542 JSONObject json = batchBidResult.results.optJSONObject(i); 543 AdWithBid adWithBid = mAdWithBidArgumentUtil.parseJsonResponse(json); 544 Uri debugReportingWinUri = 545 extractValidUri(json.optString(DEBUG_REPORTING_WIN_URI_FIELD_NAME, "")); 546 Uri debugReportingLossUri = 547 extractValidUri(json.optString(DEBUG_REPORTING_LOSS_URI_FIELD_NAME, "")); 548 549 GenerateBidResult.Builder generateBidResultBuilder = 550 GenerateBidResult.builder() 551 .setAdWithBid(adWithBid) 552 .setWinDebugReportUri(debugReportingWinUri) 553 .setLossDebugReportUri(debugReportingLossUri); 554 555 if (mCpcBillingEnabled) { 556 double adCost = json.optDouble(AD_COST_FIELD_NAME); 557 if (!Double.isNaN(adCost) && !Double.isInfinite(adCost)) { 558 generateBidResultBuilder.setAdCost( 559 new AdCost(adCost, NUM_BITS_STOCHASTIC_ROUNDING)); 560 } 561 } 562 results.add(generateBidResultBuilder.build()); 563 } 564 } catch (IllegalArgumentException e) { 565 sLogger.w( 566 e, 567 "Invalid ad with bid returned by a generateBid script. Returning empty" 568 + " list of ad with bids."); 569 runAdBiddingPerCAExecutionLogger.setGenerateBidJsScriptResultCode( 570 JS_RUN_STATUS_OUTPUT_SEMANTIC_ERROR); 571 return ImmutableList.of(); 572 } 573 574 runAdBiddingPerCAExecutionLogger.setGenerateBidJsScriptResultCode(JS_RUN_STATUS_SUCCESS); 575 return results.build(); 576 } 577 578 /** 579 * Parses the output from the invocation of the {@code scoreAd} JS function on a list of ad with 580 * associated bids {@link Double}. The script output has been pre-parsed into an {@link 581 * AuctionScriptResult} object that will contain the script sstatus code and the list of scores. 582 * The method will return an empty list of ads if the status code is not {@link 583 * JSScriptEngineCommonConstants#JS_SCRIPT_STATUS_SUCCESS} or if there has been any problem 584 * parsing the JS response. 585 */ 586 private List<ScoreAdResult> handleScoreAdsOutput( 587 AuctionScriptResult batchBidResult, 588 AdSelectionExecutionLogger adSelectionExecutionLogger) { 589 ImmutableList.Builder<ScoreAdResult> result = ImmutableList.builder(); 590 591 if (batchBidResult.status != JS_SCRIPT_STATUS_SUCCESS) { 592 sLogger.v("Scoring script failed, returning empty result."); 593 adSelectionExecutionLogger.setScoreAdJsScriptResultCode( 594 JS_RUN_STATUS_OUTPUT_SYNTAX_ERROR); 595 } else { 596 for (int i = 0; i < batchBidResult.results.length(); i++) { 597 // If the output of the score for this advert is invalid JSON or doesn't have a 598 // score we are dropping the advert by scoring it with 0. 599 JSONObject json = batchBidResult.results.optJSONObject(i); 600 Double score = json.optDouble(AD_SCORE_FIELD_NAME, 0.0); 601 Uri debugReportingWinUri = 602 extractValidUri(json.optString(DEBUG_REPORTING_WIN_URI_FIELD_NAME, "")); 603 Uri debugReportingLossUri = 604 extractValidUri(json.optString(DEBUG_REPORTING_LOSS_URI_FIELD_NAME, "")); 605 String sellerRejectReason = 606 json.optString(DEBUG_REPORTING_SELLER_REJECT_REASON_FIELD_NAME, ""); 607 result.add( 608 ScoreAdResult.builder() 609 .setAdScore(score) 610 .setSellerRejectReason(sellerRejectReason) 611 .setWinDebugReportUri(debugReportingWinUri) 612 .setLossDebugReportUri(debugReportingLossUri) 613 .build()); 614 } 615 adSelectionExecutionLogger.setScoreAdJsScriptResultCode(JS_RUN_STATUS_SUCCESS); 616 } 617 618 adSelectionExecutionLogger.endScoreAds(); 619 return result.build(); 620 } 621 622 /** 623 * Parses the output from the invocation of the {@code selectOutcome} JS function on a list of 624 * ad selection ids {@link Double} with associated bids {@link Double}. The script output has 625 * been pre-parsed into an {@link AuctionScriptResult} object that will contain the script 626 * status code and the results as a list. This handler expects a single result in the {@code 627 * results} or an empty list which is also valid as long as {@code status} is {@link 628 * JSScriptEngineCommonConstants#JS_SCRIPT_STATUS_SUCCESS} 629 * 630 * <p>The method will return a status code is not {@link 631 * JSScriptEngineCommonConstants#JS_SCRIPT_STATUS_SUCCESS} if there has been any problem 632 * executing JS script or parsing the JS response. 633 * 634 * @throws IllegalStateException is thrown in case the status is not success or the results has 635 * more than one item. 636 */ 637 private Long handleSelectOutcomesOutput( 638 AuctionScriptResult scriptResults, 639 SelectAdsFromOutcomesExecutionLogger selectAdsFromOutcomesExecutionLogger) 640 throws IllegalStateException { 641 if (scriptResults.status != JS_SCRIPT_STATUS_SUCCESS 642 || scriptResults.results.length() != 1) { 643 String errorMsg = 644 String.format( 645 JS_EXECUTION_STATUS_UNSUCCESSFUL, 646 scriptResults.status, 647 scriptResults.results); 648 sLogger.v(errorMsg); 649 selectAdsFromOutcomesExecutionLogger.endExecutionScriptTimestamp( 650 JS_RUN_STATUS_OUTPUT_SYNTAX_ERROR); 651 throw new IllegalStateException(errorMsg); 652 } 653 654 if (scriptResults.results.isNull(0)) { 655 selectAdsFromOutcomesExecutionLogger.endExecutionScriptTimestamp(JS_RUN_STATUS_SUCCESS); 656 return null; 657 } 658 659 try { 660 JSONObject resultOutcomeJson = scriptResults.results.getJSONObject(0); 661 // Use Long class to parse from string 662 Long result = Long.valueOf( 663 resultOutcomeJson.optString(SelectAdsFromOutcomesArgumentUtil.ID_FIELD_NAME)); 664 selectAdsFromOutcomesExecutionLogger.endExecutionScriptTimestamp(JS_RUN_STATUS_SUCCESS); 665 return result; 666 } catch (JSONException e) { 667 String errorMsg = String.format(JS_EXECUTION_RESULT_INVALID, scriptResults.results); 668 sLogger.v(errorMsg); 669 selectAdsFromOutcomesExecutionLogger.endExecutionScriptTimestamp( 670 JS_RUN_STATUS_OUTPUT_SEMANTIC_ERROR); 671 throw new IllegalStateException(errorMsg); 672 } 673 } 674 675 /** 676 * Runs the function call generated by {@code auctionFunctionCallGenerator} in the JS script 677 * {@code jsScript} for the list of {code ads} provided. The function will be called by a 678 * generated extra function that is responsible for iterating through all arguments and causing 679 * an early failure if the result of the function invocations is not an object containing a 680 * 'status' field or the value of the 'status' is not 0. In case of success status is 0, if the 681 * result doesn't have a status field, status is -1 otherwise the status is the non-zero status 682 * returned by the failed invocation. The 'results' field contains the JSON array with the 683 * results of the function invocations. The parameter {@code auctionFunctionCallGenerator} is 684 * responsible for generating the call to the auction function by splitting the advert data 685 * 686 * <p>The inner function call generated by {@code auctionFunctionCallGenerator} will receive for 687 * every call one of the ads or ads with bid and the extra arguments specified using {@code 688 * otherArgs} in the order they are specified. 689 * 690 * @return A future with the result of the function or failing with {@link 691 * IllegalArgumentException} if the script is not valid, doesn't contain {@code 692 * auctionFunctionName}. 693 */ 694 ListenableFuture<AuctionScriptResult> runAuctionScriptIterative( 695 String jsScript, 696 List<JSScriptArgument> ads, 697 List<JSScriptArgument> otherArgs, 698 Function<List<JSScriptArgument>, String> auctionFunctionCallGenerator) { 699 try { 700 return transform( 701 callAuctionScript( 702 mDebugReportingScript.wrapIterativeJs(jsScript), 703 ads, 704 otherArgs, 705 auctionFunctionCallGenerator, 706 AD_SELECTION_ITERATIVE_PROCESSING_JS), 707 this::parseAuctionScriptResult, 708 mExecutor); 709 } catch (JSONException e) { 710 throw new JSExecutionException( 711 "Illegal result returned by our internal iterative calling function.", e); 712 } 713 } 714 715 ListenableFuture<AuctionScriptResult> runAuctionScriptGenerateBidV3( 716 String jsScript, 717 List<JSScriptArgument> args, 718 Function<List<JSScriptArgument>, String> auctionFunctionCallGenerator) { 719 try { 720 return transform( 721 callAuctionScript( 722 mDebugReportingScript.wrapGenerateBidsV3Js(jsScript), 723 args, 724 auctionFunctionCallGenerator, 725 AD_SELECTION_GENERATE_BID_JS_V3), 726 this::parseAuctionScriptResult, 727 mExecutor); 728 } catch (JSONException e) { 729 throw new JSExecutionException( 730 "Illegal result returned by our internal batch calling function.", e); 731 } 732 } 733 734 /** 735 * Runs the function call generated by {@code auctionFunctionCallGenerator} in the JS script 736 * {@code jsScript} for the list of {@code ads} provided. The function will be called by a 737 * generated extra function that is responsible for calling the JS script. 738 * 739 * <p>If the result of the function invocations is not an object containing a 'status' or 740 * 'results' field, or the value of the 'status' is not 0 then will return failure status. 741 * 742 * <p>In case of success status is 0. The 'results' field contains the JSON array with the 743 * results of the function invocations. The parameter {@code auctionFunctionCallGenerator} is 744 * responsible for generating the call to the auction function by passing the list of advert 745 * data 746 * 747 * <p>The inner function call generated by {@code auctionFunctionCallGenerator} will receive the 748 * list of ads and the extra arguments specified using {@code otherArgs} in the order they are 749 * specified. 750 * 751 * @return A future with the result of the function or failing with {@link 752 * IllegalArgumentException} if the script is not valid, doesn't contain {@code 753 * auctionFunctionName}. 754 */ 755 ListenableFuture<AuctionScriptResult> runAuctionScriptBatch( 756 String jsScript, 757 List<JSScriptArgument> ads, 758 List<JSScriptArgument> otherArgs, 759 Function<List<JSScriptArgument>, String> auctionFunctionCallGenerator) { 760 try { 761 return transform( 762 callAuctionScript( 763 jsScript, 764 ads, 765 otherArgs, 766 auctionFunctionCallGenerator, 767 AD_SELECTION_BATCH_PROCESSING_JS), 768 this::parseAuctionScriptResult, 769 mExecutor); 770 } catch (JSONException e) { 771 throw new JSExecutionException( 772 "Illegal result returned by our internal batch calling function.", e); 773 } 774 } 775 776 /** 777 * @return A {@link ListenableFuture} containing the result of the validation of the given 778 * {@code jsScript} script. A script is valid if it is valid JS code and it contains all the 779 * functions specified in {@code expectedFunctionsNames} are defined in the script. There is 780 * no validation of the expected signature. 781 */ 782 ListenableFuture<Boolean> validateAuctionScript( 783 String jsScript, List<String> expectedFunctionsNames) { 784 return transform( 785 mJsEngine.evaluate( 786 jsScript + "\n" + CHECK_FUNCTIONS_EXIST_JS, 787 ImmutableList.of( 788 stringArrayArg(FUNCTION_NAMES_ARG_NAME, expectedFunctionsNames)), 789 buildIsolateSettings( 790 mEnforceMaxHeapSizeFeatureSupplier, 791 mMaxHeapSizeBytesSupplier, 792 mIsolateConsoleMessageInLogsEnabled), 793 mRetryStrategy), 794 Boolean::parseBoolean, 795 mExecutor); 796 } 797 798 // TODO(b/260786980) remove the patch added to make bidding JS backward compatible 799 private ListenableFuture<List<GenerateBidResult>> handleBackwardIncompatibilityScenario( 800 String generateBidJS, 801 List<JSScriptArgument> signals, 802 List<JSScriptArgument> adDataArguments, 803 RunAdBiddingPerCAExecutionLogger runAdBiddingPerCAExecutionLogger, 804 JSExecutionException jsExecutionException) { 805 ListenableFuture<AdSelectionScriptEngine.AuctionScriptResult> biddingResult = 806 updateArgsIfNeeded(generateBidJS, signals, jsExecutionException) 807 .transformAsync( 808 args -> 809 runAuctionScriptIterative( 810 generateBidJS, 811 adDataArguments, 812 args, 813 this::callGenerateBid), 814 mExecutor); 815 return transform( 816 biddingResult, 817 result -> { 818 List<GenerateBidResult> bidResults = 819 handleGenerateBidsOutput(result, runAdBiddingPerCAExecutionLogger); 820 runAdBiddingPerCAExecutionLogger.endGenerateBids(); 821 return bidResults; 822 }, 823 mExecutor); 824 } 825 826 /** 827 * @return the number of arguments taken by {@code functionName} in a given {@code jsScript} 828 * falls-back to -1 if there is no function found the with given name. 829 */ 830 @VisibleForTesting 831 ListenableFuture<Integer> getAuctionScriptArgCount(String jsScript, String functionName) { 832 return transform( 833 mJsEngine.evaluate( 834 jsScript + "\n" + GET_FUNCTION_ARGUMENT_COUNT, 835 ImmutableList.of( 836 stringArrayArg( 837 FUNCTION_NAMES_ARG_NAME, ImmutableList.of(functionName))), 838 buildIsolateSettings( 839 mEnforceMaxHeapSizeFeatureSupplier, 840 mMaxHeapSizeBytesSupplier, 841 mIsolateConsoleMessageInLogsEnabled), 842 mRetryStrategy), 843 Integer::parseInt, 844 mExecutor); 845 } 846 847 /** 848 * @return Updates the args passed to bidding JS to maintain backward compatibility 849 * (b/259718738), this shall be removed with TODO(b/260786980). Throws back the original 850 * {@link JSExecutionException} if the js method does not match signature of the backward 851 * compat. 852 */ 853 private FluentFuture<List<JSScriptArgument>> updateArgsIfNeeded( 854 String generateBidJS, 855 List<JSScriptArgument> originalArgs, 856 JSExecutionException jsExecutionException) { 857 final int previousJSArgumentCount = 7; 858 final int previousJSUserSignalsIndex = 5; 859 860 return FluentFuture.from( 861 transform( 862 getAuctionScriptArgCount(generateBidJS, GENERATE_BID_FUNCTION_NAME), 863 argCount -> { 864 List<JSScriptArgument> updatedArgList = 865 originalArgs.stream().collect(Collectors.toList()); 866 if (argCount == previousJSArgumentCount) { 867 try { 868 // This argument needs to be placed at the second last position 869 updatedArgList.add( 870 previousJSUserSignalsIndex, 871 jsonArg( 872 USER_SIGNALS_ARG_NAME, 873 AdSelectionSignals.EMPTY)); 874 return updatedArgList; 875 } catch (JSONException e) { 876 sLogger.e( 877 "Could not create JS argument: %s", 878 USER_SIGNALS_ARG_NAME); 879 } 880 } 881 throw jsExecutionException; 882 }, 883 mExecutor)); 884 } 885 886 private AuctionScriptResult parseAuctionScriptResult(String auctionScriptResult) { 887 try { 888 if (auctionScriptResult.isEmpty()) { 889 throw new IllegalArgumentException( 890 "The auction script either doesn't contain the required function or the" 891 + " function returns null"); 892 } 893 894 JSONObject jsonResult = new JSONObject(auctionScriptResult); 895 896 return new AuctionScriptResult( 897 jsonResult.getInt(STATUS_FIELD_NAME), 898 jsonResult.getJSONArray(RESULTS_FIELD_NAME)); 899 } catch (JSONException e) { 900 throw new RuntimeException( 901 "Illegal result returned by our internal batch calling function.", e); 902 } 903 } 904 905 /** 906 * @return a {@link ListenableFuture} containing the string representation of a JSON object 907 * containing two fields: 908 * <p> 909 * <ul> 910 * <li>{@code status} field that will be 0 in case of successful processing of all ads or 911 * non-zero if any of the calls to processed an ad returned a non-zero status. In the 912 * last case the returned status will be the same returned in the failing invocation. 913 * The function {@code auctionFunctionName} is assumed to return a JSON object 914 * containing at least a {@code status} field. 915 * <li>{@code results} with the results of the invocation of {@code auctionFunctionName} 916 * to all the given ads. 917 * </ul> 918 * <p> 919 */ 920 private ListenableFuture<String> callAuctionScript( 921 String jsScript, 922 List<JSScriptArgument> args, 923 Function<List<JSScriptArgument>, String> auctionFunctionCallGenerator, 924 String adSelectionProcessorJS) 925 throws JSONException { 926 927 String argPassing = 928 args.stream() 929 .map(JSScriptArgument::name) 930 .collect(Collectors.joining(ARG_PASSING_SEPARATOR)); 931 932 return mJsEngine.evaluate( 933 jsScript 934 + "\n" 935 + String.format( 936 adSelectionProcessorJS, 937 argPassing, 938 auctionFunctionCallGenerator.apply(args)), 939 args, 940 buildIsolateSettings( 941 mEnforceMaxHeapSizeFeatureSupplier, 942 mMaxHeapSizeBytesSupplier, 943 mIsolateConsoleMessageInLogsEnabled), 944 mRetryStrategy); 945 } 946 947 /** 948 * @return a {@link ListenableFuture} containing the string representation of a JSON object 949 * containing two fields: 950 * <p> 951 * <ul> 952 * <li>{@code status} field that will be 0 in case of successful processing of all ads or 953 * non-zero if any of the calls to processed an ad returned a non-zero status. In the 954 * last case the returned status will be the same returned in the failing invocation. 955 * The function {@code auctionFunctionName} is assumed to return a JSON object 956 * containing at least a {@code status} field. 957 * <li>{@code results} with the results of the invocation of {@code auctionFunctionName} 958 * to all the given ads. 959 * </ul> 960 * <p> 961 */ 962 private ListenableFuture<String> callAuctionScript( 963 String jsScript, 964 List<JSScriptArgument> adverts, 965 List<JSScriptArgument> otherArgs, 966 Function<List<JSScriptArgument>, String> auctionFunctionCallGenerator, 967 String adSelectionProcessorJS) 968 throws JSONException { 969 ImmutableList.Builder<JSScriptArgument> advertsArg = ImmutableList.builder(); 970 advertsArg.addAll(adverts); 971 sLogger.v( 972 "script: %s%nadverts: %s%nother args: %s%nprocessor script: %s%n", 973 jsScript, advertsArg, otherArgs, adSelectionProcessorJS); 974 975 List<JSScriptArgument> allArgs = 976 ImmutableList.<JSScriptArgument>builder() 977 .add(arrayArg(ADS_ARG_NAME, advertsArg.build())) 978 .addAll(otherArgs) 979 .build(); 980 981 String argPassing = 982 allArgs.stream() 983 .map(JSScriptArgument::name) 984 .collect(Collectors.joining(ARG_PASSING_SEPARATOR)); 985 986 return mJsEngine.evaluate( 987 jsScript 988 + "\n" 989 + String.format( 990 adSelectionProcessorJS, 991 argPassing, 992 auctionFunctionCallGenerator.apply(otherArgs)), 993 allArgs, 994 buildIsolateSettings( 995 mEnforceMaxHeapSizeFeatureSupplier, 996 mMaxHeapSizeBytesSupplier, 997 mIsolateConsoleMessageInLogsEnabled), 998 mRetryStrategy); 999 } 1000 1001 private String callGenerateBid(List<JSScriptArgument> otherArgs) { 1002 // The first argument is the local variable "ad" defined in AD_SELECTION_BATCH_PROCESSING_JS 1003 StringBuilder callArgs = new StringBuilder(AD_VAR_NAME); 1004 for (JSScriptArgument currArg : otherArgs) { 1005 callArgs.append(String.format(",%s", currArg.name())); 1006 } 1007 return String.format(GENERATE_BID_FUNCTION_NAME + "(%s)", callArgs); 1008 } 1009 1010 private String callGenerateBidV3(List<JSScriptArgument> args) { 1011 return String.format( 1012 GENERATE_BID_FUNCTION_NAME + "(%s)", 1013 args.stream() 1014 .map(JSScriptArgument::name) 1015 .collect(Collectors.joining(ARG_PASSING_SEPARATOR))); 1016 } 1017 1018 private String callScoreAd(List<JSScriptArgument> otherArgs) { 1019 StringBuilder callArgs = 1020 new StringBuilder( 1021 String.format( 1022 "%s.%s, %s.%s", 1023 AD_VAR_NAME, 1024 AdWithBidArgumentUtil.AD_FIELD_NAME, 1025 AD_VAR_NAME, 1026 AdWithBidArgumentUtil.BID_FIELD_NAME)); 1027 for (JSScriptArgument currArg : otherArgs) { 1028 callArgs.append(String.format(",%s", currArg.name())); 1029 } 1030 return String.format(SCORE_AD_FUNCTION_NAME + "(%s)", callArgs); 1031 } 1032 1033 private String callSelectOutcome(List<JSScriptArgument> otherArgs) { 1034 StringBuilder callArgs = new StringBuilder(ADS_ARG_NAME); 1035 for (JSScriptArgument currArg : otherArgs) { 1036 callArgs.append(String.format(",%s", currArg.name())); 1037 } 1038 return String.format("selectOutcome(%s)", callArgs); 1039 } 1040 1041 JSScriptArgument translateCustomAudience(DBCustomAudience customAudience) throws JSONException { 1042 ImmutableList.Builder<JSScriptArgument> adsArg = ImmutableList.builder(); 1043 for (DBAdData ad : customAudience.getAds()) { 1044 adsArg.add(mAdDataArgumentUtil.asRecordArgument("ignored", ad)); 1045 } 1046 // TODO(b/273357664): Verify with product on the set of fields we want to include. 1047 return recordArg( 1048 CUSTOM_AUDIENCE_ARG_NAME, 1049 stringArg("owner", customAudience.getOwner()), 1050 stringArg("name", customAudience.getName()), 1051 jsonArg("userBiddingSignals", customAudience.getUserBiddingSignals()), 1052 arrayArg("ads", adsArg.build())); 1053 } 1054 1055 public static class AuctionScriptResult { 1056 public final int status; 1057 public final JSONArray results; 1058 1059 public AuctionScriptResult(int status, JSONArray results) { 1060 this.status = status; 1061 this.results = results; 1062 } 1063 } 1064 1065 private static Uri extractValidUri(String uriString) { 1066 if (Strings.isNullOrEmpty(uriString) || !URLUtil.isValidUrl(uriString)) { 1067 return Uri.EMPTY; 1068 } 1069 return Uri.parse(uriString); 1070 } 1071 1072 private static IsolateSettings buildIsolateSettings( 1073 Supplier<Boolean> enforceMaxHeapSizeFeatureSupplier, 1074 Supplier<Long> maxHeapSizeBytesSupplier, 1075 Supplier<Boolean> isolateConsoleMessageInLogsEnabled) { 1076 return IsolateSettings.builder() 1077 .setEnforceMaxHeapSizeFeature(enforceMaxHeapSizeFeatureSupplier.get()) 1078 .setMaxHeapSizeBytes(maxHeapSizeBytesSupplier.get()) 1079 .setIsolateConsoleMessageInLogsEnabled(isolateConsoleMessageInLogsEnabled.get()) 1080 .build(); 1081 } 1082 } 1083