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 static com.android.adservices.service.js.JSScriptEngineCommonConstants.WASM_MODULE_BYTES_ID;
20 
21 import android.annotation.Nullable;
22 import android.annotation.SuppressLint;
23 import android.content.Context;
24 
25 import androidx.javascriptengine.IsolateStartupParameters;
26 import androidx.javascriptengine.JavaScriptIsolate;
27 import androidx.javascriptengine.JavaScriptSandbox;
28 import androidx.javascriptengine.MemoryLimitExceededException;
29 import androidx.javascriptengine.SandboxDeadException;
30 
31 import com.android.adservices.LoggerFactory;
32 import com.android.adservices.concurrency.AdServicesExecutors;
33 import com.android.adservices.service.common.RetryStrategy;
34 import com.android.adservices.service.exception.JSExecutionException;
35 import com.android.adservices.service.profiling.JSScriptEngineLogConstants;
36 import com.android.adservices.service.profiling.Profiler;
37 import com.android.adservices.service.profiling.StopWatch;
38 import com.android.adservices.service.profiling.Tracing;
39 import com.android.adservices.shared.common.ApplicationContextSingleton;
40 import com.android.internal.annotations.VisibleForTesting;
41 
42 import com.google.common.base.Preconditions;
43 import com.google.common.util.concurrent.ClosingFuture;
44 import com.google.common.util.concurrent.FluentFuture;
45 import com.google.common.util.concurrent.FutureCallback;
46 import com.google.common.util.concurrent.Futures;
47 import com.google.common.util.concurrent.ListenableFuture;
48 import com.google.common.util.concurrent.ListeningExecutorService;
49 
50 import java.io.Closeable;
51 import java.util.List;
52 import java.util.Set;
53 
54 import javax.annotation.concurrent.GuardedBy;
55 
56 /**
57  * A convenience class to execute JS scripts using a JavaScriptSandbox. Because arguments to the
58  * {@link #evaluate(String, List, IsolateSettings)} methods are set at JavaScriptSandbox level,
59  * calls to that methods are serialized to avoid one scripts being able to interfere one another.
60  *
61  * <p>The class is re-entrant, for best performance when using it on multiple thread is better to
62  * have every thread using its own instance.
63  */
64 public class JSScriptEngine {
65 
66     @VisibleForTesting public static final String TAG = JSScriptEngine.class.getSimpleName();
67 
68     public static final String NON_SUPPORTED_MAX_HEAP_SIZE_EXCEPTION_MSG =
69             "JS isolate does not support Max heap size";
70     public static final String JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG =
71             "Unable to create isolate";
72     public static final String JS_SCRIPT_ENGINE_SANDBOX_DEAD_MSG =
73             "Unable to evaluate on isolate due to sandbox dead exception";
74     public static final String JS_EVALUATE_METHOD_NAME = "JSScriptEngine#evaluate";
75     public static final Set<Class<? extends Exception>> RETRYABLE_EXCEPTIONS_FROM_JS_ENGINE =
76             Set.of(JSScriptEngineConnectionException.class);
77     private static final Object sJSScriptEngineLock = new Object();
78 
79     @SuppressLint("StaticFieldLeak")
80     @GuardedBy("sJSScriptEngineLock")
81     private static JSScriptEngine sSingleton;
82 
83     private final Context mContext;
84     private final JavaScriptSandboxProvider mJsSandboxProvider;
85     private final ListeningExecutorService mExecutorService;
86     private final Profiler mProfiler;
87     private final LoggerFactory.Logger mLogger;
88 
89     /**
90      * Extracting the logic to create the JavaScriptSandbox in a factory class for better
91      * testability. This factory class creates a single instance of {@link JavaScriptSandbox} until
92      * the instance is invalidated by calling {@link
93      * JavaScriptSandboxProvider#destroyCurrentInstance()}. The instance is returned wrapped in a
94      * {@code Future}
95      *
96      * <p>Throws {@link JSSandboxIsNotAvailableException} if JS Sandbox is not available in the
97      * current version of the WebView
98      */
99     @VisibleForTesting
100     static class JavaScriptSandboxProvider {
101         private final Object mSandboxLock = new Object();
102         private StopWatch mSandboxInitStopWatch;
103         private final Profiler mProfiler;
104         private final LoggerFactory.Logger mLogger;
105 
106         @GuardedBy("mSandboxLock")
107         private FluentFuture<JavaScriptSandbox> mFutureSandbox;
108 
JavaScriptSandboxProvider(Profiler profiler, LoggerFactory.Logger logger)109         JavaScriptSandboxProvider(Profiler profiler, LoggerFactory.Logger logger) {
110             mProfiler = profiler;
111             mLogger = logger;
112         }
113 
getFutureInstance(Context context)114         public FluentFuture<JavaScriptSandbox> getFutureInstance(Context context) {
115             synchronized (mSandboxLock) {
116                 if (mFutureSandbox == null) {
117                     if (!AvailabilityChecker.isJSSandboxAvailable()) {
118                         JSSandboxIsNotAvailableException exception =
119                                 new JSSandboxIsNotAvailableException();
120                         mLogger.e(
121                                 exception,
122                                 "JS Sandbox is not available in this version of WebView "
123                                         + "or WebView is not installed at all!");
124                         mFutureSandbox =
125                                 FluentFuture.from(Futures.immediateFailedFuture(exception));
126                         return mFutureSandbox;
127                     }
128 
129                     mLogger.d("Creating JavaScriptSandbox");
130                     mSandboxInitStopWatch =
131                             mProfiler.start(JSScriptEngineLogConstants.SANDBOX_INIT_TIME);
132 
133                     mFutureSandbox =
134                             FluentFuture.from(
135                                     JavaScriptSandbox.createConnectedInstanceAsync(
136                                             // This instance will have the same lifetime
137                                             // of the PPAPI process
138                                             context.getApplicationContext()));
139 
140                     mFutureSandbox.addCallback(
141                             new FutureCallback<JavaScriptSandbox>() {
142                                 @Override
143                                 public void onSuccess(JavaScriptSandbox result) {
144                                     mSandboxInitStopWatch.stop();
145                                     mLogger.d("JSScriptEngine created.");
146                                 }
147 
148                                 @Override
149                                 public void onFailure(Throwable t) {
150                                     mSandboxInitStopWatch.stop();
151                                     mLogger.e(t, "JavaScriptSandbox initialization failed");
152                                 }
153                             },
154                             AdServicesExecutors.getLightWeightExecutor());
155                 }
156 
157                 return mFutureSandbox;
158             }
159         }
160 
destroyIfCurrentInstance( JavaScriptSandbox javaScriptSandbox)161         public ListenableFuture<Void> destroyIfCurrentInstance(
162                 JavaScriptSandbox javaScriptSandbox) {
163             mLogger.d("Destroying specific instance of JavaScriptSandbox");
164             synchronized (mSandboxLock) {
165                 if (mFutureSandbox != null) {
166                     ListenableFuture<JavaScriptSandbox> futureSandbox = mFutureSandbox;
167                     return mFutureSandbox
168                             .<Void>transform(
169                                     jsSandbox -> {
170                                         synchronized (mSandboxLock) {
171                                             if (mFutureSandbox != futureSandbox) {
172                                                 mLogger.d(
173                                                         "mFutureSandbox is already set to a"
174                                                                 + " different future which "
175                                                                 + "indicates"
176                                                                 + " this is not the active "
177                                                                 + "sandbox");
178                                                 return null;
179                                             }
180                                             if (jsSandbox == javaScriptSandbox) {
181                                                 mLogger.d(
182                                                         "Closing connection from JSScriptEngine to"
183                                                             + " JavaScriptSandbox as the sandbox"
184                                                             + " requested is the current instance");
185                                                 jsSandbox.close();
186                                                 mFutureSandbox = null;
187                                             } else {
188                                                 mLogger.d(
189                                                         "Not closing the connection from"
190                                                             + " JSScriptEngine to JavaScriptSandbox"
191                                                             + "  as this is not the same instance"
192                                                             + " as requested");
193                                             }
194                                             return null;
195                                         }
196                                     },
197                                     AdServicesExecutors.getLightWeightExecutor())
198                             .catching(
199                                     Throwable.class,
200                                     t -> {
201                                         mLogger.e(
202                                                 t,
203                                                 "JavaScriptSandbox initialization failed,"
204                                                         + " cannot close");
205                                         return null;
206                                     },
207                                     AdServicesExecutors.getLightWeightExecutor());
208                 } else {
209                     return Futures.immediateVoidFuture();
210                 }
211             }
212         }
213 
214         /**
215          * Closes the connection with {@code JavaScriptSandbox}. Any running computation will fail.
216          * A new call to {@link #getFutureInstance(Context)} will create the instance again.
217          */
218         public ListenableFuture<Void> destroyCurrentInstance() {
219             mLogger.d("Destroying JavaScriptSandbox");
220             synchronized (mSandboxLock) {
221                 if (mFutureSandbox != null) {
222                     ListenableFuture<Void> result =
223                             mFutureSandbox
224                                     .<Void>transform(
225                                             jsSandbox -> {
226                                                 mLogger.d(
227                                                         "Closing connection from JSScriptEngine to"
228                                                                 + " JavaScriptSandbox");
229                                                 jsSandbox.close();
230                                                 return null;
231                                             },
232                                             AdServicesExecutors.getLightWeightExecutor())
233                                     .catching(
234                                             Throwable.class,
235                                             t -> {
236                                                 mLogger.e(
237                                                         t,
238                                                         "JavaScriptSandbox initialization failed,"
239                                                                 + " cannot close");
240                                                 return null;
241                                             },
242                                             AdServicesExecutors.getLightWeightExecutor());
243                     mFutureSandbox = null;
244                     return result;
245                 } else {
246                     return Futures.immediateVoidFuture();
247                 }
248             }
249         }
250     }
251 
252     /**
253      * @return JSScriptEngine instance
254      */
255     public static JSScriptEngine getInstance(LoggerFactory.Logger logger) {
256         return getInstance(ApplicationContextSingleton.get(), logger);
257     }
258 
259     /**
260      * @return JSScriptEngine instance
261      */
262     @VisibleForTesting
263     public static JSScriptEngine getInstance(Context context, LoggerFactory.Logger logger) {
264         synchronized (sJSScriptEngineLock) {
265             if (sSingleton == null) {
266                 logger.d("Creating new instance for JSScriptEngine");
267                 Profiler profiler = Profiler.createNoOpInstance(TAG);
268                 sSingleton =
269                         new JSScriptEngine(
270                                 context,
271                                 new JavaScriptSandboxProvider(profiler, logger),
272                                 profiler,
273                                 // There is no blocking call or IO code in the service logic
274                                 AdServicesExecutors.getLightWeightExecutor(),
275                                 logger);
276             }
277 
278             return sSingleton;
279         }
280     }
281 
282     /**
283      * @return a singleton JSScriptEngine instance with the given profiler
284      * @throws IllegalStateException if an existing instance exists
285      */
286     @VisibleForTesting
287     public static JSScriptEngine getInstanceForTesting(
288             Context context, Profiler profiler, LoggerFactory.Logger logger) {
289         synchronized (sJSScriptEngineLock) {
290             // If there is no instance already created or the instance was shutdown
291             if (sSingleton != null) {
292                 throw new IllegalStateException(
293                         "Unable to initialize test JSScriptEngine multiple times using"
294                                 + "the real JavaScriptSandboxProvider.");
295             }
296             logger.d("Creating new instance for JSScriptEngine");
297             sSingleton =
298                     new JSScriptEngine(
299                             context,
300                             new JavaScriptSandboxProvider(profiler, logger),
301                             profiler,
302                             AdServicesExecutors.getLightWeightExecutor(),
303                             logger);
304         }
305 
306         return sSingleton;
307     }
308 
309     /**
310      * This method will instantiate a new instance of JSScriptEngine every time. It is intended to
311      * be used with a fake/mock {@link JavaScriptSandboxProvider}. Using a real one would cause
312      * exception when trying to create the second instance of {@link JavaScriptSandbox}.
313      *
314      * @return a new JSScriptEngine instance
315      */
316     @VisibleForTesting
317     public static JSScriptEngine createNewInstanceForTesting(
318             Context context,
319             JavaScriptSandboxProvider jsSandboxProvider,
320             Profiler profiler,
321             LoggerFactory.Logger logger) {
322         return new JSScriptEngine(
323                 context,
324                 jsSandboxProvider,
325                 profiler,
326                 AdServicesExecutors.getLightWeightExecutor(),
327                 logger);
328     }
329 
330     /**
331      * Closes the connection with JavaScriptSandbox. Any running computation will be terminated. It
332      * is not necessary to recreate instances of {@link JSScriptEngine} after this call; new calls
333      * to {@code evaluate} for existing instance will cause the connection to WV to be restored if
334      * necessary.
335      *
336      * @return A future to be used by tests needing to know when the sandbox close call happened.
337      */
338     public ListenableFuture<Void> shutdown() {
339         return FluentFuture.from(mJsSandboxProvider.destroyCurrentInstance())
340                 .transformAsync(
341                         ignored -> {
342                             synchronized (sJSScriptEngineLock) {
343                                 sSingleton = null;
344                             }
345                             mLogger.d("shutdown successful for JSScriptEngine");
346                             return Futures.immediateVoidFuture();
347                         },
348                         mExecutorService)
349                 .catching(
350                         Throwable.class,
351                         throwable -> {
352                             mLogger.e(throwable, "shutdown unsuccessful for JSScriptEngine");
353                             throw new IllegalStateException(
354                                     "Shutdown unsuccessful for JSScriptEngine", throwable);
355                         },
356                         mExecutorService);
357     }
358 
359     @VisibleForTesting
360     @SuppressWarnings("FutureReturnValueIgnored")
361     JSScriptEngine(
362             Context context,
363             JavaScriptSandboxProvider jsSandboxProvider,
364             Profiler profiler,
365             ListeningExecutorService executorService,
366             LoggerFactory.Logger logger) {
367         this.mContext = context;
368         this.mJsSandboxProvider = jsSandboxProvider;
369         this.mProfiler = profiler;
370         this.mExecutorService = executorService;
371         this.mLogger = logger;
372         // Forcing initialization of JavaScriptSandbox
373         jsSandboxProvider.getFutureInstance(mContext);
374     }
375 
376     /**
377      * Same as {@link #evaluate(String, List, String, IsolateSettings, RetryStrategy)} where the
378      * entry point function name is {@link JSScriptEngineCommonConstants#ENTRY_POINT_FUNC_NAME}.
379      */
380     public ListenableFuture<String> evaluate(
381             String jsScript,
382             List<JSScriptArgument> args,
383             IsolateSettings isolateSettings,
384             RetryStrategy retryStrategy) {
385         return evaluate(
386                 jsScript,
387                 args,
388                 JSScriptEngineCommonConstants.ENTRY_POINT_FUNC_NAME,
389                 isolateSettings,
390                 retryStrategy);
391     }
392 
393     /**
394      * Invokes the function {@code entryFunctionName} defined by the JS code in {@code jsScript} and
395      * return the result. It will reset the JavaScriptSandbox status after evaluating the script.
396      *
397      * @param jsScript The JS script
398      * @param args The arguments to pass when invoking {@code entryFunctionName}
399      * @param entryFunctionName The name of a function defined in {@code jsScript} that should be
400      *     invoked.
401      * @return A {@link ListenableFuture} containing the JS string representation of the result of
402      *     {@code entryFunctionName}'s invocation
403      */
404     public ListenableFuture<String> evaluate(
405             String jsScript,
406             List<JSScriptArgument> args,
407             String entryFunctionName,
408             IsolateSettings isolateSettings,
409             RetryStrategy retryStrategy) {
410         return evaluateInternal(
411                 jsScript, args, entryFunctionName, null, isolateSettings, retryStrategy, true);
412     }
413 
414     /**
415      * Invokes the JS code in {@code jsScript} and return the result. It will reset the
416      * JavaScriptSandbox status after evaluating the script.
417      *
418      * @param jsScript The JS script
419      * @return A {@link ListenableFuture} containing the JS string representation of the result of
420      *     {@code entryFunctionName}'s invocation
421      */
422     public ListenableFuture<String> evaluate(
423             String jsScript, IsolateSettings isolateSettings, RetryStrategy retryStrategy) {
424         return evaluateInternal(
425                 jsScript, List.of(), "", null, isolateSettings, retryStrategy, false);
426     }
427 
428     /**
429      * Loads the WASM module defined by {@code wasmBinary}, invokes the function {@code
430      * entryFunctionName} defined by the JS code in {@code jsScript} and return the result. It will
431      * reset the JavaScriptSandbox status after evaluating the script. The function is expected to
432      * accept all the arguments defined in {@code args} plus an extra final parameter of type {@code
433      * WebAssembly.Module}.
434      *
435      * @param jsScript The JS script
436      * @param args The arguments to pass when invoking {@code entryFunctionName}
437      * @param entryFunctionName The name of a function defined in {@code jsScript} that should be
438      *     invoked.
439      * @return A {@link ListenableFuture} containing the JS string representation of the result of
440      *     {@code entryFunctionName}'s invocation
441      */
442     public ListenableFuture<String> evaluate(
443             String jsScript,
444             byte[] wasmBinary,
445             List<JSScriptArgument> args,
446             String entryFunctionName,
447             IsolateSettings isolateSettings,
448             RetryStrategy retryStrategy) {
449         return evaluateInternal(
450                 jsScript,
451                 args,
452                 entryFunctionName,
453                 wasmBinary,
454                 isolateSettings,
455                 retryStrategy,
456                 true);
457     }
458 
459     private ListenableFuture<String> evaluateInternal(
460             String jsScript,
461             List<JSScriptArgument> args,
462             String entryFunctionName,
463             @Nullable byte[] wasmBinary,
464             IsolateSettings isolateSettings,
465             RetryStrategy retryStrategy,
466             boolean generateEntryPointWrapper) {
467         return retryStrategy.call(
468                 () ->
469                         ClosingFuture.from(mJsSandboxProvider.getFutureInstance(mContext))
470                                 .transformAsync(
471                                         (closer, jsSandbox) ->
472                                                 evaluateOnSandbox(
473                                                         closer,
474                                                         jsSandbox,
475                                                         jsScript,
476                                                         args,
477                                                         entryFunctionName,
478                                                         wasmBinary,
479                                                         isolateSettings,
480                                                         generateEntryPointWrapper),
481                                         mExecutorService)
482                                 .finishToFuture(),
483                 RETRYABLE_EXCEPTIONS_FROM_JS_ENGINE,
484                 mLogger,
485                 JS_EVALUATE_METHOD_NAME);
486     }
487 
488     private ClosingFuture<String> evaluateOnSandbox(
489             ClosingFuture.DeferredCloser closer,
490             JavaScriptSandbox jsSandbox,
491             String jsScript,
492             List<JSScriptArgument> args,
493             String entryFunctionName,
494             @Nullable byte[] wasmBinary,
495             IsolateSettings isolateSettings,
496             boolean generateEntryPointWrapper) {
497 
498         boolean hasWasmModule = wasmBinary != null;
499         if (hasWasmModule) {
500             Preconditions.checkState(
501                     isWasmSupported(jsSandbox),
502                     "Cannot evaluate a JS script depending on WASM on the JS"
503                             + " Sandbox available on this device");
504         }
505 
506         JavaScriptIsolate jsIsolate = createIsolate(jsSandbox, isolateSettings);
507         closer.eventuallyClose(new CloseableIsolateWrapper(jsIsolate, mLogger), mExecutorService);
508 
509         if (hasWasmModule) {
510             mLogger.d(
511                     "Evaluating JS script with associated WASM on thread %s",
512                     Thread.currentThread().getName());
513             try {
514                 jsIsolate.provideNamedData(WASM_MODULE_BYTES_ID, wasmBinary);
515             } catch (IllegalStateException ise) {
516                 mLogger.d(ise, "Unable to pass WASM byte array to JS Isolate");
517                 throw new JSExecutionException("Unable to pass WASM byte array to JS Isolate", ise);
518             }
519         } else {
520             mLogger.d("Evaluating JS script on thread %s", Thread.currentThread().getName());
521         }
522 
523         if (isolateSettings.getIsolateConsoleMessageInLogsEnabled()) {
524             if (isConsoleCallbackSupported(jsSandbox)) {
525                 mLogger.d("Logging console messages from Javascript Isolate.");
526                 jsIsolate.setConsoleCallback(
527                         mExecutorService,
528                         consoleMessage ->
529                                 mLogger.v(
530                                         "Javascript Console Message: %s",
531                                         consoleMessage.toString()));
532             } else {
533                 mLogger.d("Logging console messages from Javascript Isolate is not available.");
534             }
535         } else {
536             mLogger.d("Logging console messages from Javascript Isolate is disabled.");
537         }
538 
539         StringBuilder fullScript = new StringBuilder(jsScript);
540         if (generateEntryPointWrapper) {
541             String entryPointCall =
542                     JSScriptEngineCommonCodeGenerator.generateEntryPointCallingCode(
543                             args, entryFunctionName, hasWasmModule);
544 
545             fullScript.append("\n");
546             fullScript.append(entryPointCall);
547         }
548         mLogger.v("Calling JavaScriptSandbox for script %s", fullScript);
549 
550         StopWatch jsExecutionStopWatch =
551                 mProfiler.start(JSScriptEngineLogConstants.JAVA_EXECUTION_TIME);
552         int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX);
553         return ClosingFuture.from(jsIsolate.evaluateJavaScriptAsync(fullScript.toString()))
554                 .transform(
555                         (ignoredCloser, result) -> {
556                             jsExecutionStopWatch.stop();
557                             mLogger.v("JavaScriptSandbox result is " + result);
558                             Tracing.endAsyncSection(
559                                     Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX, traceCookie);
560                             return result;
561                         },
562                         mExecutorService)
563                 .catching(
564                         Exception.class,
565                         (ignoredCloser, exception) -> {
566                             mLogger.v(
567                                     "Failure running JS in JavaScriptSandbox: "
568                                             + exception.getMessage());
569                             jsExecutionStopWatch.stop();
570                             Tracing.endAsyncSection(
571                                     Tracing.JSSCRIPTENGINE_EVALUATE_ON_SANDBOX, traceCookie);
572                             if (exception instanceof SandboxDeadException) {
573                                 /*
574                                    Although we are already checking for this during createIsolate
575                                    method, the creation might be successful in edge cases.
576                                    However the evaluation will fail with SandboxDeadException.
577                                    Whenever we encounter this error, we should ensure to destroy
578                                    the current instance as all other evaluations will fail.
579                                 */
580                                 mJsSandboxProvider.destroyIfCurrentInstance(jsSandbox);
581                                 throw new JSScriptEngineConnectionException(
582                                         JS_SCRIPT_ENGINE_SANDBOX_DEAD_MSG, exception);
583                             } else if (exception instanceof MemoryLimitExceededException) {
584                                 /*
585                                   In case of androidx.javascriptengine.MemoryLimitExceededException
586                                   we should not retry the JS Evaluation but close the current
587                                   instance of Javascript Sandbox.
588                                 */
589                                 mJsSandboxProvider.destroyIfCurrentInstance(jsSandbox);
590                             }
591                             throw new JSExecutionException(
592                                     "Failure running JS in JavaScriptSandbox: "
593                                             + exception.getMessage(),
594                                     exception);
595                         },
596                         mExecutorService);
597     }
598 
599     private boolean isWasmSupported(JavaScriptSandbox jsSandbox) {
600         boolean wasmCompilationSupported =
601                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_WASM_COMPILATION);
602         // We will pass the WASM binary via `provideNamesData`
603         // The JS will read the data using android.consumeNamedDataAsArrayBuffer
604         boolean provideConsumeArrayBufferSupported =
605                 jsSandbox.isFeatureSupported(
606                         JavaScriptSandbox.JS_FEATURE_PROVIDE_CONSUME_ARRAY_BUFFER);
607         // The call android.consumeNamedDataAsArrayBuffer to read the WASM byte array
608         // returns a promises so all our code will be in a promise chain
609         boolean promiseReturnSupported =
610                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_PROMISE_RETURN);
611         mLogger.v(
612                 String.format(
613                         "Is WASM supported? WASM_COMPILATION: %b  PROVIDE_CONSUME_ARRAY_BUFFER: %b,"
614                                 + " PROMISE_RETURN: %b",
615                         wasmCompilationSupported,
616                         provideConsumeArrayBufferSupported,
617                         promiseReturnSupported));
618         return wasmCompilationSupported
619                 && provideConsumeArrayBufferSupported
620                 && promiseReturnSupported;
621     }
622 
623     /**
624      * @return a future value indicating if the JS Sandbox installed on the device supports console
625      *     message callback.
626      */
627     @VisibleForTesting
628     public ListenableFuture<Boolean> isConsoleCallbackSupported() {
629         return mJsSandboxProvider
630                 .getFutureInstance(mContext)
631                 .transform(this::isConsoleCallbackSupported, mExecutorService);
632     }
633 
634     private boolean isConsoleCallbackSupported(JavaScriptSandbox javaScriptSandbox) {
635         boolean isConsoleCallbackSupported =
636                 javaScriptSandbox.isFeatureSupported(
637                         JavaScriptSandbox.JS_FEATURE_CONSOLE_MESSAGING);
638         mLogger.v("isConsoleCallbackSupported: %b", isConsoleCallbackSupported);
639         return isConsoleCallbackSupported;
640     }
641 
642     /**
643      * @return a future value indicating if the JS Sandbox installed on the device supports WASM
644      *     execution or an error if the connection to the JS Sandbox failed.
645      */
646     public ListenableFuture<Boolean> isWasmSupported() {
647         return mJsSandboxProvider
648                 .getFutureInstance(mContext)
649                 .transform(this::isWasmSupported, mExecutorService);
650     }
651 
652     /**
653      * @return a future value indicating if the JS Sandbox installed on the device supports
654      *     configurable Heap size.
655      */
656     @VisibleForTesting
657     public ListenableFuture<Boolean> isConfigurableHeapSizeSupported() {
658         return mJsSandboxProvider
659                 .getFutureInstance(mContext)
660                 .transform(this::isConfigurableHeapSizeSupported, mExecutorService);
661     }
662 
663     private boolean isConfigurableHeapSizeSupported(JavaScriptSandbox jsSandbox) {
664         boolean isConfigurableHeapSupported =
665                 jsSandbox.isFeatureSupported(JavaScriptSandbox.JS_FEATURE_ISOLATE_MAX_HEAP_SIZE);
666         mLogger.v("Is configurable max heap size supported? : %b", isConfigurableHeapSupported);
667         return isConfigurableHeapSupported;
668     }
669 
670     /**
671      * @return a future value indicating if the JS Sandbox installed on the device supports
672      *     evaluation without transaction limits.
673      */
674     public ListenableFuture<Boolean> isLargeTransactionsSupported() {
675         return mJsSandboxProvider
676                 .getFutureInstance(mContext)
677                 .transform(this::isLargeTransactionsSupported, mExecutorService);
678     }
679 
680     private boolean isLargeTransactionsSupported(JavaScriptSandbox javaScriptSandbox) {
681         boolean isLargeTransactionsSupported =
682                 javaScriptSandbox.isFeatureSupported(
683                         JavaScriptSandbox.JS_FEATURE_EVALUATE_WITHOUT_TRANSACTION_LIMIT);
684         mLogger.v(
685                 "Is evaluate without transaction limit supported? : %b",
686                 isLargeTransactionsSupported);
687         return isLargeTransactionsSupported;
688     }
689 
690     /**
691      * Creates a new isolate. This method handles the case where the `JavaScriptSandbox` process has
692      * been terminated by closing this connection. The ongoing call will fail, we won't try to
693      * recover it to keep the code simple.
694      *
695      * <p>Throws error in case, we have enforced max heap memory restrictions and isolate does not
696      * support that feature
697      */
698     private JavaScriptIsolate createIsolate(
699             JavaScriptSandbox jsSandbox, IsolateSettings isolateSettings) {
700         // TODO: b/321237839. After upgrading the dependency on javascriptengine to beta1, revisit
701         // the exception handling of this method.
702         int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_CREATE_ISOLATE);
703         StopWatch isolateStopWatch =
704                 mProfiler.start(JSScriptEngineLogConstants.ISOLATE_CREATE_TIME);
705         try {
706             if (!isConfigurableHeapSizeSupported(jsSandbox)
707                     && isolateSettings.getEnforceMaxHeapSizeFeature()) {
708                 mLogger.e("Memory limit enforcement required, but not supported by Isolate");
709                 throw new IllegalStateException(NON_SUPPORTED_MAX_HEAP_SIZE_EXCEPTION_MSG);
710             }
711 
712             JavaScriptIsolate javaScriptIsolate;
713             if (isolateSettings.getEnforceMaxHeapSizeFeature()
714                     && isolateSettings.getMaxHeapSizeBytes() > 0) {
715                 mLogger.d(
716                         "Creating JS isolate with memory limit: %d bytes",
717                         isolateSettings.getMaxHeapSizeBytes());
718                 IsolateStartupParameters startupParams = new IsolateStartupParameters();
719                 startupParams.setMaxHeapSizeBytes(isolateSettings.getMaxHeapSizeBytes());
720                 javaScriptIsolate = jsSandbox.createIsolate(startupParams);
721             } else {
722                 mLogger.d("Creating JS isolate with unbounded memory limit");
723                 javaScriptIsolate = jsSandbox.createIsolate();
724             }
725             return javaScriptIsolate;
726         } catch (RuntimeException jsSandboxPossiblyDisconnected) {
727             mLogger.e(
728                     jsSandboxPossiblyDisconnected,
729                     "JavaScriptSandboxProcess is threw exception, cannot create an isolate to run"
730                             + " JS code into (Disconnected?). Resetting connection with"
731                             + " AwJavaScriptSandbox to enable future calls.");
732             mJsSandboxProvider.destroyIfCurrentInstance(jsSandbox);
733             throw new JSScriptEngineConnectionException(
734                     JS_SCRIPT_ENGINE_CONNECTION_EXCEPTION_MSG, jsSandboxPossiblyDisconnected);
735         } finally {
736             isolateStopWatch.stop();
737             Tracing.endAsyncSection(Tracing.JSSCRIPTENGINE_CREATE_ISOLATE, traceCookie);
738         }
739     }
740 
741     /**
742      * Checks if JS Sandbox is available in the WebView version that is installed on the device
743      * before attempting to create it. Attempting to create JS Sandbox when it's not available
744      * results in returning of a null value.
745      */
746     public static class AvailabilityChecker {
747 
748         /**
749          * @return true if JS Sandbox is available in the current WebView version, false otherwise.
750          */
751         public static boolean isJSSandboxAvailable() {
752             return JavaScriptSandbox.isSupported();
753         }
754     }
755 
756     /**
757      * Wrapper class required to convert an {@link java.lang.AutoCloseable} {@link
758      * JavaScriptIsolate} into a {@link Closeable} type.
759      */
760     private static class CloseableIsolateWrapper implements Closeable {
761         final JavaScriptIsolate mIsolate;
762 
763         final LoggerFactory.Logger mLogger;
764 
765         CloseableIsolateWrapper(JavaScriptIsolate isolate, LoggerFactory.Logger logger) {
766             mIsolate = isolate;
767             mLogger = logger;
768         }
769 
770         @Override
771         public void close() {
772             int traceCookie = Tracing.beginAsyncSection(Tracing.JSSCRIPTENGINE_CLOSE_ISOLATE);
773             mLogger.d("Closing JavaScriptSandbox isolate");
774             // Closing the isolate will also cause the thread in JavaScriptSandbox to be terminated
775             // if
776             // still running.
777             // There is no need to verify if ISOLATE_TERMINATION is supported by JavaScriptSandbox
778             // because there is no new API but just new capability on the JavaScriptSandbox side for
779             // existing API.
780             mIsolate.close();
781             Tracing.endAsyncSection(Tracing.JSSCRIPTENGINE_CLOSE_ISOLATE, traceCookie);
782         }
783     }
784 }
785