• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.adservices.service.js;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.SuppressLint;
22 import android.content.Context;
23 
24 import androidx.javascriptengine.IsolateStartupParameters;
25 import androidx.javascriptengine.JavaScriptIsolate;
26 import androidx.javascriptengine.JavaScriptSandbox;
27 
28 import com.android.adservices.LogUtil;
29 import com.android.adservices.concurrency.AdServicesExecutors;
30 import com.android.adservices.service.exception.JSExecutionException;
31 import com.android.adservices.service.profiling.JSScriptEngineLogConstants;
32 import com.android.adservices.service.profiling.Profiler;
33 import com.android.adservices.service.profiling.StopWatch;
34 import com.android.adservices.service.profiling.Tracing;
35 
36 import com.google.common.annotations.VisibleForTesting;
37 import com.google.common.base.Preconditions;
38 import com.google.common.util.concurrent.ClosingFuture;
39 import com.google.common.util.concurrent.FluentFuture;
40 import com.google.common.util.concurrent.FutureCallback;
41 import com.google.common.util.concurrent.Futures;
42 import com.google.common.util.concurrent.ListenableFuture;
43 import com.google.common.util.concurrent.ListeningExecutorService;
44 
45 import java.io.Closeable;
46 import java.util.List;
47 import java.util.Objects;
48 import java.util.stream.Collectors;
49 
50 import javax.annotation.concurrent.GuardedBy;
51 
52 /**
53  * A convenience class to execute JS scripts using a WebView. Because arguments to the {@link
54  * #evaluate(String, List, IsolateSettings)} methods are set at WebView level, calls to that methods
55  * are serialized to avoid one scripts being able to interfere one another.
56  *
57  * <p>The class is re-entrant, for best performance when using it on multiple thread is better to
58  * have every thread using its own instance.
59  */
60 public class JSScriptEngine {
61     public static final String ENTRY_POINT_FUNC_NAME = "__rb_entry_point";
62 
63     @VisibleForTesting public static final String TAG = JSScriptEngine.class.getSimpleName();
64     public static final String WASM_MODULE_BYTES_ID = "__wasmModuleBytes";
65     public static final String WASM_MODULE_ARG_NAME = "wasmModule";
66 
67     public static final String NON_SUPPORTED_MAX_HEAP_SIZE_EXCEPTION_MSG =
68             "JS isolate does not support Max heap size";
69     public static final String JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG =
70             "Unable to create isolate";
71 
72     @SuppressLint("StaticFieldLeak")
73     private static JSScriptEngine sSingleton;
74 
75     @NonNull private final Context mContext;
76     @NonNull private final JavaScriptSandboxProvider mJsSandboxProvider;
77     @NonNull private final ListeningExecutorService mExecutorService;
78     @NonNull private final Profiler mProfiler;
79 
80     /**
81      * Extracting the logic to create the JavaScriptSandbox in a factory class for better
82      * testability. This factory class creates a single instance of {@link JavaScriptSandbox} until
83      * the instance is invalidated by calling {@link
84      * JavaScriptSandboxProvider#destroyCurrentInstance()}. The instance is returned wrapped in a
85      * {@code Future}
86      *
87      * <p>Throws {@link JSSandboxIsNotAvailableException} if JS Sandbox is not available in the
88      * current version of the WebView
89      */
90     @VisibleForTesting
91     static class JavaScriptSandboxProvider {
92         private final Object mSandboxLock = new Object();
93         private StopWatch mSandboxInitStopWatch;
94         private Profiler mProfiler;
95 
96         @GuardedBy("mSandboxLock")
97         private FluentFuture<JavaScriptSandbox> mFutureSandbox;
98 
JavaScriptSandboxProvider(Profiler profiler)99         JavaScriptSandboxProvider(Profiler profiler) {
100             mProfiler = profiler;
101         }
102 
getFutureInstance(Context context)103         public FluentFuture<JavaScriptSandbox> getFutureInstance(Context context) {
104             synchronized (mSandboxLock) {
105                 if (mFutureSandbox == null) {
106                     if (!AvailabilityChecker.isJSSandboxAvailable()) {
107                         LogUtil.e(
108                                 "JS Sandbox is not available in this version of WebView "
109                                         + "or WebView is not installed at all!");
110                         throw new JSSandboxIsNotAvailableException();
111                     }
112 
113                     LogUtil.i("Creating JavaScriptSandbox");
114                     mSandboxInitStopWatch =
115                             mProfiler.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
116 
117                     mFutureSandbox =
118                             FluentFuture.from(
119                                     JavaScriptSandbox.createConnectedInstanceAsync(
120                                             // This instance will have the same lifetime
121                                             // of the PPAPI process
122                                             context.getApplicationContext()));
123 
124                     mFutureSandbox.addCallback(
125                             new FutureCallback<JavaScriptSandbox>() {
126                                 @Override
127                                 public void onSuccess(JavaScriptSandbox result) {
128                                     mSandboxInitStopWatch.stop();
129                                     LogUtil.i("JSScriptEngine created.");
130                                 }
131 
132                                 @Override
133                                 public void onFailure(Throwable t) {
134                                     mSandboxInitStopWatch.stop();
135                                     LogUtil.e(t, "JavaScriptSandbox initialization failed");
136                                 }
137                             },
138                             AdServicesExecutors.getLightWeightExecutor());
139                 }
140 
141                 return mFutureSandbox;
142             }
143         }
144 
145         /**
146          * Closes the connection with {@code JavaScriptSandbox}. Any running computation will fail.
147          * A new call to {@link #getFutureInstance(Context)} will create the instance again.
148          */
destroyCurrentInstance()149         public ListenableFuture<Void> destroyCurrentInstance() {
150             synchronized (mSandboxLock) {
151                 if (mFutureSandbox != null) {
152                     ListenableFuture<Void> result =
153                             FluentFuture.from(mFutureSandbox)
154                                     .<Void>transform(
155                                             jsSandbox -> {
156                                                 LogUtil.i(
157                                                         "Closing connection from JSScriptEngine to"
158                                                                 + " WebView Sandbox");
159                                                 jsSandbox.close();
160                                                 return null;
161                                             },
162                                             AdServicesExecutors.getLightWeightExecutor())
163                                     .catching(
164                                             Throwable.class,
165                                             t -> {
166                                                 LogUtil.i(
167                                                         "JavaScriptSandbox initialization failed,"
168                                                                 + " won't close");
169                                                 return null;
170                                             },
171                                             AdServicesExecutors.getLightWeightExecutor());
172                     mFutureSandbox = null;
173                     return result;
174                 } else {
175                     return Futures.immediateVoidFuture();
176                 }
177             }
178         }
179     }
180 
181     /**
182      * @return JSScriptEngine instance
183      */
getInstance(@onNull Context context)184     public static JSScriptEngine getInstance(@NonNull Context context) {
185         synchronized (JSScriptEngine.class) {
186             if (sSingleton == null) {
187                 Profiler profiler = Profiler.createNoOpInstance(TAG);
188                 sSingleton =
189                         new JSScriptEngine(
190                                 context,
191                                 new JavaScriptSandboxProvider(profiler),
192                                 profiler,
193                                 // There is no blocking call or IO code in the service logic
194                                 AdServicesExecutors.getLightWeightExecutor());
195             }
196 
197             return sSingleton;
198         }
199     }
200 
201     /**
202      * @return a singleton JSScriptEngine instance with the given profiler
203      * @throws IllegalStateException if an existing instance exists
204      */
205     @VisibleForTesting
getInstanceForTesting( @onNull Context context, @NonNull Profiler profiler)206     public static JSScriptEngine getInstanceForTesting(
207             @NonNull Context context, @NonNull Profiler profiler) {
208         synchronized (JSScriptEngine.class) {
209             // If there is no instance already created or the instance was shutdown
210             if (sSingleton != null) {
211                 throw new IllegalStateException(
212                         "Unable to initialize test JSScriptEngine multiple times using"
213                                 + "the real JavaScriptSandboxProvider.");
214             }
215 
216             sSingleton =
217                     new JSScriptEngine(
218                             context,
219                             new JavaScriptSandboxProvider(profiler),
220                             profiler,
221                             AdServicesExecutors.getLightWeightExecutor());
222         }
223 
224         return sSingleton;
225     }
226 
227     /**
228      * This method will instantiate a new instance of JSScriptEngine every time. It is intended to
229      * be used with a fake/mock {@link JavaScriptSandboxProvider}. Using a real one would cause
230      * exception when trying to create the second instance of {@link JavaScriptSandbox}.
231      *
232      * @return a new JSScriptEngine instance
233      */
234     @VisibleForTesting
createNewInstanceForTesting( @onNull Context context, @NonNull JavaScriptSandboxProvider jsSandboxProvider, @NonNull Profiler profiler)235     public static JSScriptEngine createNewInstanceForTesting(
236             @NonNull Context context,
237             @NonNull JavaScriptSandboxProvider jsSandboxProvider,
238             @NonNull Profiler profiler) {
239         return new JSScriptEngine(
240                 context, jsSandboxProvider, profiler, AdServicesExecutors.getLightWeightExecutor());
241     }
242 
243     /**
244      * Closes the connection with WebView. Any running computation will be terminated. It is not
245      * necessary to recreate instances of {@link JSScriptEngine} after this call; new calls to
246      * {@code evaluate} for existing instance will cause the connection to WV to be restored if
247      * necessary.
248      *
249      * @return A future to be used by tests needing to know when the sandbox close call happened.
250      */
shutdown()251     public ListenableFuture<Void> shutdown() {
252         return mJsSandboxProvider.destroyCurrentInstance();
253     }
254 
255     @VisibleForTesting
256     @SuppressWarnings("FutureReturnValueIgnored")
JSScriptEngine( @onNull Context context, @NonNull JavaScriptSandboxProvider jsSandboxProvider, @NonNull Profiler profiler, @NonNull ListeningExecutorService executorService)257     JSScriptEngine(
258             @NonNull Context context,
259             @NonNull JavaScriptSandboxProvider jsSandboxProvider,
260             @NonNull Profiler profiler,
261             @NonNull ListeningExecutorService executorService) {
262         Objects.requireNonNull(context);
263         Objects.requireNonNull(jsSandboxProvider);
264         Objects.requireNonNull(profiler);
265         Objects.requireNonNull(executorService);
266 
267         this.mContext = context;
268         this.mJsSandboxProvider = jsSandboxProvider;
269         this.mProfiler = profiler;
270         this.mExecutorService = executorService;
271         // Forcing initialization of WebView
272         jsSandboxProvider.getFutureInstance(mContext);
273     }
274 
275     /**
276      * Same as {@link #evaluate(String, List, String, IsolateSettings)} where the entry point
277      * function name is {@link #ENTRY_POINT_FUNC_NAME}.
278      */
279     @NonNull
evaluate( @onNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull IsolateSettings isolateSettings)280     public ListenableFuture<String> evaluate(
281             @NonNull String jsScript,
282             @NonNull List<JSScriptArgument> args,
283             @NonNull IsolateSettings isolateSettings) {
284         return evaluate(jsScript, args, ENTRY_POINT_FUNC_NAME, isolateSettings);
285     }
286 
287     /**
288      * Invokes the function {@code entryFunctionName} defined by the JS code in {@code jsScript} and
289      * return the result. It will reset the WebView status after evaluating the script.
290      *
291      * @param jsScript The JS script
292      * @param args The arguments to pass when invoking {@code entryFunctionName}
293      * @param entryFunctionName The name of a function defined in {@code jsScript} that should be
294      *     invoked.
295      * @return A {@link ListenableFuture} containing the JS string representation of the result of
296      *     {@code entryFunctionName}'s invocation
297      */
298     @NonNull
evaluate( @onNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String entryFunctionName, @NonNull IsolateSettings isolateSettings)299     public ListenableFuture<String> evaluate(
300             @NonNull String jsScript,
301             @NonNull List<JSScriptArgument> args,
302             @NonNull String entryFunctionName,
303             @NonNull IsolateSettings isolateSettings) {
304         return evaluateInternal(jsScript, args, entryFunctionName, null, isolateSettings);
305     }
306 
307     /**
308      * Loads the WASM module defined by {@code wasmBinary}, invokes the function {@code
309      * entryFunctionName} defined by the JS code in {@code jsScript} and return the result. It will
310      * reset the WebView status after evaluating the script. The function is expected to accept all
311      * the arguments defined in {@code args} plus an extra final parameter of type {@code
312      * WebAssembly.Module}.
313      *
314      * @param jsScript The JS script
315      * @param args The arguments to pass when invoking {@code entryFunctionName}
316      * @param entryFunctionName The name of a function defined in {@code jsScript} that should be
317      *     invoked.
318      * @return A {@link ListenableFuture} containing the JS string representation of the result of
319      *     {@code entryFunctionName}'s invocation
320      */
321     @NonNull
evaluate( @onNull String jsScript, @NonNull byte[] wasmBinary, @NonNull List<JSScriptArgument> args, @NonNull String entryFunctionName, @NonNull IsolateSettings isolateSettings)322     public ListenableFuture<String> evaluate(
323             @NonNull String jsScript,
324             @NonNull byte[] wasmBinary,
325             @NonNull List<JSScriptArgument> args,
326             @NonNull String entryFunctionName,
327             @NonNull IsolateSettings isolateSettings) {
328         Objects.requireNonNull(wasmBinary);
329 
330         return evaluateInternal(jsScript, args, entryFunctionName, wasmBinary, isolateSettings);
331     }
332 
333     @NonNull
evaluateInternal( @onNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String entryFunctionName, @Nullable byte[] wasmBinary, @NonNull IsolateSettings isolateSettings)334     private ListenableFuture<String> evaluateInternal(
335             @NonNull String jsScript,
336             @NonNull List<JSScriptArgument> args,
337             @NonNull String entryFunctionName,
338             @Nullable byte[] wasmBinary,
339             @NonNull IsolateSettings isolateSettings) {
340         Objects.requireNonNull(jsScript);
341         Objects.requireNonNull(args);
342         Objects.requireNonNull(entryFunctionName);
343 
344         return ClosingFuture.from(mJsSandboxProvider.getFutureInstance(mContext))
345                 .transformAsync(
346                         (closer, jsSandbox) ->
347                                 evaluateOnSandbox(
348                                         closer,
349                                         jsSandbox,
350                                         jsScript,
351                                         args,
352                                         entryFunctionName,
353                                         wasmBinary,
354                                         isolateSettings),
355                         mExecutorService)
356                 .finishToFuture();
357     }
358 
359     @NonNull
evaluateOnSandbox( @onNull ClosingFuture.DeferredCloser closer, @NonNull JavaScriptSandbox jsSandbox, @NonNull String jsScript, @NonNull List<JSScriptArgument> args, @NonNull String entryFunctionName, @Nullable byte[] wasmBinary, @NonNull IsolateSettings isolateSettings)360     private ClosingFuture<String> evaluateOnSandbox(
361             @NonNull ClosingFuture.DeferredCloser closer,
362             @NonNull JavaScriptSandbox jsSandbox,
363             @NonNull String jsScript,
364             @NonNull List<JSScriptArgument> args,
365             @NonNull String entryFunctionName,
366             @Nullable byte[] wasmBinary,
367             @NonNull IsolateSettings isolateSettings) {
368 
369         boolean hasWasmModule = wasmBinary != null;
370         if (hasWasmModule) {
371             Preconditions.checkState(
372                     isWasmSupported(jsSandbox),
373                     "Cannot evaluate a JS script depending on WASM on the JS"
374                             + " Sandbox available on this device");
375         }
376 
377         JavaScriptIsolate jsIsolate = createIsolate(jsSandbox, isolateSettings);
378         closer.eventuallyClose(new CloseableIsolateWrapper(jsIsolate), mExecutorService);
379 
380         if (hasWasmModule) {
381             LogUtil.d(
382                     "Evaluating JS script with associated WASM on thread %s",
383                     Thread.currentThread().getName());
384 
385             if (!jsIsolate.provideNamedData(WASM_MODULE_BYTES_ID, wasmBinary)) {
386                 throw new JSExecutionException("Unable to pass WASM byte array to JS Isolate");
387             }
388         } else {
389             LogUtil.d("Evaluating JS script on thread %s", Thread.currentThread().getName());
390         }
391 
392         String entryPointCall = callEntryPoint(args, entryFunctionName, hasWasmModule);
393 
394         String fullScript = jsScript + "\n" + entryPointCall;
395         LogUtil.v("Calling WebView for script %s", fullScript);
396 
397         StopWatch jsExecutionStopWatch =
398                 mProfiler.start(JSScriptEngineLogConstants.JAVA_EXECUTION_TIME);
399         int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX);
400         return ClosingFuture.from(jsIsolate.evaluateJavaScriptAsync(fullScript))
401                 .transform(
402                         (ignoredCloser, result) -> {
403                             jsExecutionStopWatch.stop();
404                             LogUtil.v("WebView result is " + result);
405                             Tracing.endAsyncSection(
406                                     Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX, traceCookie);
407                             return result;
408                         },
409                         mExecutorService)
410                 .catching(
411                         Exception.class,
412                         (ignoredCloser, exception) -> {
413                             jsExecutionStopWatch.stop();
414                             Tracing.endAsyncSection(
415                                     Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX, traceCookie);
416                             throw new JSExecutionException(
417                                     "Failure running JS in WebView: " + exception.getMessage(),
418                                     exception);
419                         },
420                         mExecutorService);
421     }
422 
isWasmSupported(JavaScriptSandbox jsSandbox)423     private boolean isWasmSupported(JavaScriptSandbox jsSandbox) {
424         boolean wasmCompilationSupported =
425                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_WASM_COMPILATION);
426         // We will pass the WASM binary via `provideNamesData`
427         // The JS will read the data using android.consumeNamedDataAsArrayBuffer
428         boolean provideConsumeArrayBufferSupported =
429                 jsSandbox.isFeatureSupported(
430                         JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER);
431         // The call android.consumeNamedDataAsArrayBuffer to read the WASM byte array
432         // returns a promises so all our code will be in a promise chain
433         boolean promiseReturnSupported =
434                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN);
435         LogUtil.v(
436                 String.format(
437                         "Is WASM supported? WASM_COMPILATION: %b  PROVIDE_CONSUME_ARRAY_BUFFER: %b,"
438                                 + " PROMISE_RETURN: %b",
439                         wasmCompilationSupported,
440                         provideConsumeArrayBufferSupported,
441                         promiseReturnSupported));
442         return wasmCompilationSupported
443                 && provideConsumeArrayBufferSupported
444                 && promiseReturnSupported;
445     }
446 
447     /**
448      * @return a future value indicating if the JS Sandbox installed on the device supports WASM
449      *     execution or an error if the connection to the JS Sandbox failed.
450      */
isWasmSupported()451     public ListenableFuture<Boolean> isWasmSupported() {
452         return mJsSandboxProvider
453                 .getFutureInstance(mContext)
454                 .transform(this::isWasmSupported, mExecutorService);
455     }
456 
isConfigurableHeapSizeSupported(JavaScriptSandbox jsSandbox)457     boolean isConfigurableHeapSizeSupported(JavaScriptSandbox jsSandbox) {
458         boolean isConfigurableHeapSupported =
459                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE);
460         LogUtil.v("Is configurable max heap size supported? : %b", isConfigurableHeapSupported);
461         return isConfigurableHeapSupported;
462     }
463 
464     /**
465      * Creates a new isolate. This method handles the case where the `JavaScriptSandbox` process has
466      * been terminated by closing this connection. The ongoing call will fail, we won't try to
467      * recover it to keep the code simple.
468      *
469      * <p>Throws error in case, we have enforced max heap memory restrictions and isolate does not
470      * support that feature
471      */
createIsolate( JavaScriptSandbox jsSandbox, IsolateSettings isolateSettings)472     private JavaScriptIsolate createIsolate(
473             JavaScriptSandbox jsSandbox, IsolateSettings isolateSettings) {
474         int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_CREATE_ISOLATE);
475         StopWatch isolateStopWatch =
476                 mProfiler.start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
477         try {
478             if (!isConfigurableHeapSizeSupported(jsSandbox)
479                     && isolateSettings.getEnforceMaxHeapSizeFeature()) {
480                 LogUtil.e("Memory limit enforcement required, but not supported by Isolate");
481                 throw new IllegalStateException(NON_SUPPORTED_MAX_HEAP_SIZE_EXCEPTION_MSG);
482             }
483 
484             JavaScriptIsolate javaScriptIsolate;
485             if (isolateSettings.getEnforceMaxHeapSizeFeature()
486                     && isolateSettings.getMaxHeapSizeBytes() > 0) {
487                 LogUtil.d(
488                         "Creating JS isolate with memory limit: %d bytes",
489                         isolateSettings.getMaxHeapSizeBytes());
490                 IsolateStartupParameters startupParams = new IsolateStartupParameters();
491                 startupParams.setMaxHeapSizeBytes(isolateSettings.getMaxHeapSizeBytes());
492                 javaScriptIsolate = jsSandbox.createIsolate(startupParams);
493                 if (javaScriptIsolate == null) {
494                     throw new IllegalStateException(
495                             "JS Isolate does not support setting max heap size");
496                 }
497             } else {
498                 LogUtil.d("Creating JS isolate with unbounded memory limit");
499                 javaScriptIsolate = jsSandbox.createIsolate();
500             }
501             return javaScriptIsolate;
502         } catch (IllegalStateException isolateMemoryLimitUnsupported) {
503             LogUtil.e(
504                     "JavaScriptIsolate does not support setting max heap size, cannot create an"
505                             + " isolate to run JS code into.");
506             throw new JSScriptEngineConnectionException(
507                     JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG, isolateMemoryLimitUnsupported);
508         } catch (RuntimeException jsSandboxIsDisconnected) {
509             LogUtil.e(
510                     "JavaScriptSandboxProcess is disconnected, cannot create an isolate to run JS"
511                             + " code into. Resetting connection with AwJavaScriptSandbox to enable"
512                             + " future calls.");
513             mJsSandboxProvider.destroyCurrentInstance();
514             throw new JSScriptEngineConnectionException(
515                     JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG, jsSandboxIsDisconnected);
516         } finally {
517             isolateStopWatch.stop();
518             Tracing.endAsyncSection(Tracing.JSSCRIPTENGINE_CREATE_ISOLATE, traceCookie);
519         }
520     }
521 
522     /**
523      * @return The JS code for the definition an anonymous function containing the declaration of
524      *     the value of {@code args} and the invocation of the given {@code entryFunctionName}. If
525      *     the {@code addWasmBinary} parameter is true, the target function is expected to accept an
526      *     extra final parameter 'wasmModule' of type {@code WebAssembly.Module} and the method will
527      *     return a promise.
528      */
529     @NonNull
callEntryPoint( @onNull List<JSScriptArgument> args, @NonNull String entryFunctionName, boolean addWasmBinary)530     private String callEntryPoint(
531             @NonNull List<JSScriptArgument> args,
532             @NonNull String entryFunctionName,
533             boolean addWasmBinary) {
534         StringBuilder resultBuilder = new StringBuilder("(function() {\n");
535         // Declare args as constant inside this function closure to avoid any direct access by
536         // the functions in the script we are calling.
537         for (JSScriptArgument arg : args) {
538             // Avoiding to use addJavaScriptInterface because too expensive, just
539             // declaring the string parameter as part of the script.
540             resultBuilder.append(arg.variableDeclaration());
541             resultBuilder.append("\n");
542         }
543 
544         String argumentPassing =
545                 args.stream().map(JSScriptArgument::name).collect(Collectors.joining(","));
546         if (addWasmBinary) {
547             argumentPassing += "," + WASM_MODULE_ARG_NAME;
548             resultBuilder.append(
549                     String.format(
550                             "return android.consumeNamedDataAsArrayBuffer(\"%s\")"
551                                     + ".then((__value) => {\n"
552                                     + " return WebAssembly.compile(__value).then((%s) => {\n",
553                             WASM_MODULE_BYTES_ID, WASM_MODULE_ARG_NAME));
554         }
555 
556         // Call entryFunctionName with the constants just declared as parameters
557         resultBuilder.append(
558                 String.format(
559                         "return JSON.stringify(%s(%s));\n", entryFunctionName, argumentPassing));
560 
561         if (addWasmBinary) {
562             resultBuilder.append("})});\n");
563         }
564         resultBuilder.append("})();\n");
565 
566         return resultBuilder.toString();
567     }
568 
569     /**
570      * Checks if JS Sandbox is available in the WebView version that is installed on the device
571      * before attempting to create it. Attempting to create JS Sandbox when it's not available
572      * results in returning of a null value.
573      */
574     public static class AvailabilityChecker {
575 
576         /**
577          * @return true if JS Sandbox is available in the current WebView version, false otherwise.
578          */
isJSSandboxAvailable()579         public static boolean isJSSandboxAvailable() {
580             return JavaScriptSandbox.isSupported();
581         }
582     }
583 
584     /**
585      * Wrapper class required to convert an {@link java.lang.AutoCloseable} {@link
586      * JavaScriptIsolate} into a {@link Closeable} type.
587      */
588     private static class CloseableIsolateWrapper implements Closeable {
589         @NonNull final JavaScriptIsolate mIsolate;
590 
CloseableIsolateWrapper(@onNull JavaScriptIsolate isolate)591         CloseableIsolateWrapper(@NonNull JavaScriptIsolate isolate) {
592             Objects.requireNonNull(isolate);
593             mIsolate = isolate;
594         }
595 
596         @Override
close()597         public void close() {
598             int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_CLOSE_ISOLATE);
599             LogUtil.d("Closing WebView isolate");
600             // Closing the isolate will also cause the thread in WebView to be terminated if
601             // still running.
602             // There is no need to verify if ISOLATE_TERMINATION is supported by WebView
603             // because there is no new API but just new capability on the WebView side for
604             // existing API.
605             mIsolate.close();
606             Tracing.endAsyncSection(Tracing.JSSCRIPTENGINE_CLOSE_ISOLATE, traceCookie);
607         }
608     }
609 }
610