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