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