1 /* 2 * Copyright 2018 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 androidx.webkit.test.common; 18 19 import static org.junit.Assert.assertFalse; 20 import static org.junit.Assert.assertTrue; 21 import static org.junit.Assert.fail; 22 23 import android.annotation.SuppressLint; 24 import android.content.Context; 25 import android.graphics.Bitmap; 26 import android.graphics.Canvas; 27 import android.net.Uri; 28 import android.os.Looper; 29 import android.os.SystemClock; 30 import android.webkit.CookieManager; 31 import android.webkit.ValueCallback; 32 import android.webkit.WebChromeClient; 33 import android.webkit.WebSettings; 34 import android.webkit.WebView; 35 import android.webkit.WebViewClient; 36 37 import androidx.annotation.CallSuper; 38 import androidx.concurrent.futures.ResolvableFuture; 39 import androidx.test.core.app.ApplicationProvider; 40 import androidx.webkit.ScriptHandler; 41 import androidx.webkit.WebMessageCompat; 42 import androidx.webkit.WebMessagePortCompat; 43 import androidx.webkit.WebSettingsCompat; 44 import androidx.webkit.WebViewClientCompat; 45 import androidx.webkit.WebViewCompat; 46 import androidx.webkit.WebViewRenderProcessClient; 47 48 import org.jspecify.annotations.NonNull; 49 import org.jspecify.annotations.Nullable; 50 51 import java.util.Set; 52 import java.util.concurrent.Callable; 53 import java.util.concurrent.Executor; 54 import java.util.concurrent.TimeoutException; 55 56 /** 57 * A wrapper around a WebView instance, to run View methods on the UI thread. This also includes 58 * static helper methods related to the UI thread. 59 * 60 * This should remain functionally equivalent to android.webkit.cts.WebViewOnUiThread. 61 * Modifications to this class should be reflected in that class as necessary. See 62 * http://go/modifying-webview-cts. 63 */ 64 public class WebViewOnUiThread implements AutoCloseable { 65 /** 66 * The maximum time, in milliseconds (10 seconds) to wait for a load 67 * to be triggered. 68 */ 69 private static final long LOAD_TIMEOUT = 10000; 70 71 /** 72 * Set to true after onPageFinished is called. 73 */ 74 private boolean mLoaded; 75 76 /** 77 * The progress, in percentage, of the page load. Valid values are between 78 * 0 and 100. 79 */ 80 private int mProgress; 81 82 /** 83 * The WebView that calls will be made on. 84 */ 85 private WebView mWebView; 86 87 /** 88 * Whether mWebView is owned by this instance, and should be destroyed in cleanUp. 89 */ 90 private boolean mOwnsWebView; 91 92 /** 93 * Optional extra steps to execute during cleanup. 94 */ 95 private Runnable mCleanupTask; 96 97 /** 98 * Create a new WebViewOnUiThread that owns its own WebView instance. 99 */ WebViewOnUiThread()100 public WebViewOnUiThread() { 101 this(createWebView(), true); 102 } 103 104 /** 105 * Create a new WebViewOnUiThread wrapping the provided {@link WebView}. 106 * 107 * The caller is responsible for destroying the WebView instance. 108 */ WebViewOnUiThread(final @NonNull WebView webView)109 public WebViewOnUiThread(final @NonNull WebView webView) { 110 this(webView, false); 111 } 112 WebViewOnUiThread(final WebView webView, final boolean ownsWebView)113 private WebViewOnUiThread(final WebView webView, final boolean ownsWebView) { 114 WebkitUtils.onMainThreadSync(() -> { 115 mWebView = webView; 116 mOwnsWebView = ownsWebView; 117 mWebView.setWebViewClient(new WaitForLoadedClient(WebViewOnUiThread.this)); 118 mWebView.setWebChromeClient(new WaitForProgressClient(WebViewOnUiThread.this)); 119 }); 120 } 121 122 @Override close()123 public void close() throws Exception { 124 cleanUp(); 125 } 126 127 private static class Holder { 128 volatile WebView mView; 129 } 130 createWebView()131 public static @NonNull WebView createWebView() { 132 final Holder h = new Holder(); 133 final Context ctx = ApplicationProvider.getApplicationContext(); 134 WebkitUtils.onMainThreadSync(() -> { 135 h.mView = new WebView(ctx); 136 }); 137 return h.mView; 138 } 139 140 /** 141 * Called after a test is complete and the WebView should be disengaged from 142 * the tests. 143 * 144 * If the associated webview is owned by this object, then it will be destroyed. 145 * It is the caller's responsibility to ensure that it has been detached from 146 * the view hierarchy, if needed. 147 */ cleanUp()148 public void cleanUp() { 149 if (mCleanupTask != null) { 150 mCleanupTask.run(); 151 } 152 WebkitUtils.onMainThreadSync(() -> { 153 mWebView.clearHistory(); 154 mWebView.clearCache(true); 155 mWebView.setWebChromeClient(null); 156 mWebView.setWebViewClient(null); 157 if (mOwnsWebView) { 158 mWebView.destroy(); 159 } 160 }); 161 } 162 163 /** 164 * set a task that will be executed before any other cleanup code. 165 * 166 * The task will be executed on the same thread that executes the cleanup. 167 */ setCleanupTask(@ullable Runnable cleanupTask)168 public void setCleanupTask(@Nullable Runnable cleanupTask) { 169 mCleanupTask = cleanupTask; 170 } 171 172 /** 173 * Called from WaitForLoadedClient. 174 */ 175 // TODO(crbug.com/384100250): Refactor this class to not use synchronized methods 176 @SuppressWarnings({"EmptyMethod", "BanSynchronizedMethods"}) onPageStarted()177 synchronized void onPageStarted() { 178 } 179 180 /** 181 * Called from WaitForLoadedClient, this is used to indicate that 182 * the page is loaded, but not drawn yet. 183 */ 184 // TODO(crbug.com/384100250): Refactor this class to not use synchronized methods 185 @SuppressWarnings("BanSynchronizedMethods") onPageFinished()186 synchronized void onPageFinished() { 187 mLoaded = true; 188 this.notifyAll(); 189 } 190 191 /** 192 * Called from the WebChrome client, this sets the current progress 193 * for a page. 194 * 195 * @param progress The progress made so far between 0 and 100. 196 */ 197 // TODO(crbug.com/384100250): Refactor this class to not use synchronized methods 198 @SuppressWarnings("BanSynchronizedMethods") onProgressChanged(int progress)199 synchronized void onProgressChanged(int progress) { 200 mProgress = progress; 201 this.notifyAll(); 202 } 203 destroy(final @NonNull WebView webView)204 public static void destroy(final @NonNull WebView webView) { 205 WebkitUtils.onMainThreadSync(webView::destroy); 206 } 207 setWebViewClient(final @NonNull WebViewClient webviewClient)208 public void setWebViewClient(final @NonNull WebViewClient webviewClient) { 209 setWebViewClient(mWebView, webviewClient); 210 } 211 setWebViewClient( final @NonNull WebView webView, final @NonNull WebViewClient webviewClient)212 public static void setWebViewClient( 213 final @NonNull WebView webView, final @NonNull WebViewClient webviewClient) { 214 WebkitUtils.onMainThreadSync(() -> webView.setWebViewClient(webviewClient)); 215 } 216 setWebChromeClient(final @Nullable WebChromeClient webChromeClient)217 public void setWebChromeClient(final @Nullable WebChromeClient webChromeClient) { 218 setWebChromeClient(mWebView, webChromeClient); 219 } 220 setWebChromeClient( final @NonNull WebView webView, final @Nullable WebChromeClient webChromeClient)221 public static void setWebChromeClient( 222 final @NonNull WebView webView, final @Nullable WebChromeClient webChromeClient) { 223 WebkitUtils.onMainThreadSync(() -> webView.setWebChromeClient(webChromeClient)); 224 } 225 setWebViewRenderProcessClient( final @NonNull WebViewRenderProcessClient webViewRenderProcessClient)226 public void setWebViewRenderProcessClient( 227 final @NonNull WebViewRenderProcessClient webViewRenderProcessClient) { 228 setWebViewRenderProcessClient(mWebView, webViewRenderProcessClient); 229 } 230 setWebViewRenderProcessClient( final @NonNull WebView webView, final @NonNull WebViewRenderProcessClient webViewRenderProcessClient)231 public static void setWebViewRenderProcessClient( 232 final @NonNull WebView webView, 233 final @NonNull WebViewRenderProcessClient webViewRenderProcessClient) { 234 WebkitUtils.onMainThreadSync(() -> WebViewCompat.setWebViewRenderProcessClient( 235 webView, webViewRenderProcessClient)); 236 } 237 setWebViewRenderProcessClient( final @NonNull Executor executor, final @NonNull WebViewRenderProcessClient webViewRenderProcessClient)238 public void setWebViewRenderProcessClient( 239 final @NonNull Executor executor, 240 final @NonNull WebViewRenderProcessClient webViewRenderProcessClient) { 241 setWebViewRenderProcessClient(mWebView, executor, webViewRenderProcessClient); 242 } 243 setWebViewRenderProcessClient( final @NonNull WebView webView, final @NonNull Executor executor, final @NonNull WebViewRenderProcessClient webViewRenderProcessClient)244 public static void setWebViewRenderProcessClient( 245 final @NonNull WebView webView, 246 final @NonNull Executor executor, 247 final @NonNull WebViewRenderProcessClient webViewRenderProcessClient) { 248 WebkitUtils.onMainThreadSync(() -> WebViewCompat.setWebViewRenderProcessClient( 249 webView, executor, webViewRenderProcessClient)); 250 } 251 getWebViewRenderProcessClient()252 public @Nullable WebViewRenderProcessClient getWebViewRenderProcessClient() { 253 return getWebViewRenderProcessClient(mWebView); 254 } 255 getWebViewRenderProcessClient( final @NonNull WebView webView)256 public static @Nullable WebViewRenderProcessClient getWebViewRenderProcessClient( 257 final @NonNull WebView webView) { 258 return WebkitUtils.onMainThreadSync( 259 () -> WebViewCompat.getWebViewRenderProcessClient(webView)); 260 } 261 createWebMessageChannelCompat()262 public WebMessagePortCompat @NonNull [] createWebMessageChannelCompat() { 263 return WebkitUtils.onMainThreadSync(() -> WebViewCompat.createWebMessageChannel(mWebView)); 264 } 265 postWebMessageCompat(final @NonNull WebMessageCompat message, final @NonNull Uri targetOrigin)266 public void postWebMessageCompat(final @NonNull WebMessageCompat message, 267 final @NonNull Uri targetOrigin) { 268 WebkitUtils.onMainThreadSync( 269 () -> WebViewCompat.postWebMessage(mWebView, message, targetOrigin)); 270 } 271 addWebMessageListener(@onNull String jsObjectName, @NonNull Set<String> allowedOriginRules, final WebViewCompat.@NonNull WebMessageListener listener)272 public void addWebMessageListener(@NonNull String jsObjectName, 273 @NonNull Set<String> allowedOriginRules, 274 final WebViewCompat.@NonNull WebMessageListener listener) { 275 WebkitUtils.onMainThreadSync(() -> WebViewCompat.addWebMessageListener( 276 mWebView, jsObjectName, allowedOriginRules, listener)); 277 } 278 removeWebMessageListener(final @NonNull String jsObjectName)279 public void removeWebMessageListener(final @NonNull String jsObjectName) { 280 WebkitUtils.onMainThreadSync( 281 () -> WebViewCompat.removeWebMessageListener(mWebView, jsObjectName)); 282 } 283 284 /** 285 * @deprecated unreleased API to be removed 286 */ 287 @Deprecated 288 @SuppressWarnings("deprecation") // To be removed in 1.9.0 addDocumentStartJavaScript( @onNull String script, @NonNull Set<String> allowedOriginRules)289 public @NonNull ScriptHandler addDocumentStartJavaScript( 290 @NonNull String script, @NonNull Set<String> allowedOriginRules) { 291 return WebkitUtils.onMainThreadSync(() -> WebViewCompat.addDocumentStartJavaScript( 292 mWebView, script, allowedOriginRules)); 293 } 294 295 @SuppressLint("JavascriptInterface") addJavascriptInterface(final @NonNull Object object, final @NonNull String name)296 public void addJavascriptInterface(final @NonNull Object object, final @NonNull String name) { 297 WebkitUtils.onMainThreadSync(() -> mWebView.addJavascriptInterface(object, name)); 298 } 299 300 /** 301 * Calls loadUrl on the WebView and then waits onPageFinished 302 * and onProgressChange to reach 100. 303 * Test fails if the load timeout elapses. 304 * 305 * @param url The URL to load. 306 */ loadUrlAndWaitForCompletion(final @NonNull String url)307 public void loadUrlAndWaitForCompletion(final @NonNull String url) { 308 callAndWait(() -> mWebView.loadUrl(url)); 309 } 310 loadUrl(final @NonNull String url)311 public void loadUrl(final @NonNull String url) { 312 WebkitUtils.onMainThreadSync(() -> mWebView.loadUrl(url)); 313 } 314 315 /** 316 * Calls {@link WebView#loadData} on the WebView and then waits onPageFinished 317 * and onProgressChange to reach 100. 318 * Test fails if the load timeout elapses. 319 * 320 * @param data The data to load. 321 * @param mimeType The mimeType to pass to loadData. 322 * @param encoding The encoding to pass to loadData. 323 */ loadDataAndWaitForCompletion(final @NonNull String data, final @Nullable String mimeType, final @Nullable String encoding)324 public void loadDataAndWaitForCompletion(final @NonNull String data, 325 final @Nullable String mimeType, final @Nullable String encoding) { 326 callAndWait(() -> mWebView.loadData(data, mimeType, encoding)); 327 } 328 loadDataWithBaseURLAndWaitForCompletion(final @Nullable String baseUrl, final @NonNull String data, final @Nullable String mimeType, final @Nullable String encoding, final @Nullable String historyUrl)329 public void loadDataWithBaseURLAndWaitForCompletion(final @Nullable String baseUrl, 330 final @NonNull String data, final @Nullable String mimeType, 331 final @Nullable String encoding, 332 final @Nullable String historyUrl) { 333 callAndWait(() -> mWebView.loadDataWithBaseURL( 334 baseUrl, data, mimeType, encoding, historyUrl)); 335 } 336 337 /** 338 * Use this only when JavaScript causes a page load to wait for the 339 * page load to complete. Otherwise use loadUrlAndWaitForCompletion or 340 * similar functions. 341 */ waitForLoadCompletion()342 void waitForLoadCompletion() { 343 waitForCriteria(LOAD_TIMEOUT, this::isLoaded); 344 clearLoad(); 345 } 346 waitForCriteria(long timeout, Callable<Boolean> doneCriteria)347 private void waitForCriteria(long timeout, Callable<Boolean> doneCriteria) { 348 if (isUiThread()) { 349 waitOnUiThread(timeout, doneCriteria); 350 } else { 351 waitOnTestThread(timeout, doneCriteria); 352 } 353 } 354 getTitle()355 public @Nullable String getTitle() { 356 return WebkitUtils.onMainThreadSync(() -> mWebView.getTitle()); 357 } 358 getSettings()359 public @NonNull WebSettings getSettings() { 360 return WebkitUtils.onMainThreadSync(() -> mWebView.getSettings()); 361 } 362 getUrl()363 public @Nullable String getUrl() { 364 return WebkitUtils.onMainThreadSync(() -> mWebView.getUrl()); 365 } 366 postVisualStateCallbackCompat(final long requestId, final WebViewCompat.@NonNull VisualStateCallback callback)367 public void postVisualStateCallbackCompat(final long requestId, 368 final WebViewCompat.@NonNull VisualStateCallback callback) { 369 WebkitUtils.onMainThreadSync(() -> WebViewCompat.postVisualStateCallback( 370 mWebView, requestId, callback)); 371 } 372 373 /** 374 * Execute javascript synchronously, returning the result. 375 */ evaluateJavascriptSync(final @NonNull String script)376 public @Nullable String evaluateJavascriptSync(final @NonNull String script) { 377 final ResolvableFuture<String> future = ResolvableFuture.create(); 378 evaluateJavascript(script, future::set); 379 return WebkitUtils.waitForFuture(future); 380 } 381 evaluateJavascript(final @NonNull String script, final @Nullable ValueCallback<String> result)382 public void evaluateJavascript(final @NonNull String script, 383 final @Nullable ValueCallback<String> result) { 384 WebkitUtils.onMainThread(() -> mWebView.evaluateJavascript(script, result)); 385 } 386 getWebViewClient()387 public @NonNull WebViewClient getWebViewClient() { 388 return getWebViewClient(mWebView); 389 } 390 getWebViewClient(final @NonNull WebView webView)391 public static @NonNull WebViewClient getWebViewClient(final @NonNull WebView webView) { 392 return WebkitUtils.onMainThreadSync(() -> WebViewCompat.getWebViewClient(webView)); 393 } 394 getWebChromeClient()395 public @Nullable WebChromeClient getWebChromeClient() { 396 return getWebChromeClient(mWebView); 397 } 398 getWebChromeClient(final @NonNull WebView webView)399 public static @Nullable WebChromeClient getWebChromeClient(final @NonNull WebView webView) { 400 return WebkitUtils.onMainThreadSync(() -> WebViewCompat.getWebChromeClient(webView)); 401 } 402 getWebViewOnCurrentThread()403 public @NonNull WebView getWebViewOnCurrentThread() { 404 return mWebView; 405 } 406 407 /** 408 * Sets whether third party cookies are accepted for testing. 409 */ setAcceptThirdPartyCookies(final boolean accept)410 public void setAcceptThirdPartyCookies(final boolean accept) { 411 WebkitUtils.onMainThreadSync(() -> 412 CookieManager.getInstance().setAcceptThirdPartyCookies( 413 mWebView, accept)); 414 } 415 416 /** 417 * Wait for the current state of the DOM to be ready to render on the next draw. 418 */ waitForDOMReadyToRender()419 public void waitForDOMReadyToRender() { 420 final ResolvableFuture<Void> future = ResolvableFuture.create(); 421 postVisualStateCallbackCompat(0, requestId -> future.set(null)); 422 try { 423 WebkitUtils.waitForFuture(future); 424 } catch (RuntimeException e) { 425 if (e.getCause() instanceof TimeoutException) { 426 throw new RuntimeException( 427 "Timeout while waiting for rendering. The most likely cause is that your " 428 + "device's display is off. Enable the 'stay awake while " 429 + "charging' developer option and try again.", 430 e); 431 } else { 432 throw e; 433 } 434 } 435 } 436 437 /** 438 * Capture a bitmap representation of the current WebView state. 439 * 440 * This synchronises so that the bitmap contents reflects the current DOM state, rather than 441 * potentially capturing a previously generated frame. 442 */ captureBitmap()443 public @NonNull Bitmap captureBitmap() { 444 WebSettingsCompat.setOffscreenPreRaster(getSettings(), true); 445 waitForDOMReadyToRender(); 446 return WebkitUtils.onMainThreadSync(() -> { 447 Bitmap bitmap = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(), 448 Bitmap.Config.ARGB_8888); 449 Canvas canvas = new Canvas(bitmap); 450 mWebView.draw(canvas); 451 return bitmap; 452 }); 453 } 454 455 /** 456 * Returns true if the current thread is the UI thread based on the 457 * Looper. 458 */ isUiThread()459 private static boolean isUiThread() { 460 return (Looper.myLooper() == Looper.getMainLooper()); 461 } 462 463 /** 464 * @return Whether or not the load has finished. 465 */ 466 // TODO(crbug.com/384100250): Refactor this class to not use synchronized methods 467 @SuppressWarnings("BanSynchronizedMethods") isLoaded()468 private synchronized boolean isLoaded() { 469 return mLoaded && mProgress == 100; 470 } 471 472 /** 473 * Makes a WebView call, waits for completion and then resets the 474 * load state in preparation for the next load call. 475 * 476 * @param call The call to make on the UI thread prior to waiting. 477 */ callAndWait(Runnable call)478 private void callAndWait(Runnable call) { 479 assertFalse("WebViewOnUiThread.load*AndWaitForCompletion calls " 480 + "may not be mixed with load* calls directly on WebView " 481 + "without calling waitForLoadCompletion after the load", isLoaded()); 482 clearLoad(); // clear any extraneous signals from a previous load. 483 if (Looper.myLooper() == Looper.getMainLooper()) { 484 call.run(); 485 } else { 486 WebkitUtils.onMainThreadSync(call); 487 } 488 waitForLoadCompletion(); 489 } 490 491 /** 492 * Called whenever a load has been completed so that a subsequent call to 493 * waitForLoadCompletion doesn't return immediately. 494 */ 495 // TODO(crbug.com/384100250): Refactor this class to not use synchronized methods 496 @SuppressWarnings("BanSynchronizedMethods") clearLoad()497 private synchronized void clearLoad() { 498 mLoaded = false; 499 mProgress = 0; 500 } 501 502 /** 503 * Uses a polling mechanism, while pumping messages to check when the 504 * criteria is met. 505 */ waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria)506 private void waitOnUiThread(long timeout, final Callable<Boolean> doneCriteria) { 507 new PollingCheck(timeout) { 508 @Override 509 protected boolean check() { 510 pumpMessages(); 511 try { 512 return doneCriteria.call(); 513 } catch (Exception e) { 514 fail("Unexpected error while checking the criteria: " + e.getMessage()); 515 return true; 516 } 517 } 518 }.run(); 519 } 520 521 /** 522 * Uses a wait/notify to check when the criteria is met. 523 */ 524 // TODO(crbug.com/384100250): Refactor this class to not use synchronized methods 525 @SuppressWarnings("BanSynchronizedMethods") waitOnTestThread(long timeout, Callable<Boolean> doneCriteria)526 private synchronized void waitOnTestThread(long timeout, Callable<Boolean> doneCriteria) { 527 try { 528 long waitEnd = SystemClock.uptimeMillis() + timeout; 529 long timeRemaining = timeout; 530 while (!doneCriteria.call() && timeRemaining > 0) { 531 this.wait(timeRemaining); 532 timeRemaining = waitEnd - SystemClock.uptimeMillis(); 533 } 534 assertTrue("Action failed to complete before timeout", doneCriteria.call()); 535 } catch (InterruptedException e) { 536 // We'll just drop out of the loop and fail 537 } catch (Exception e) { 538 fail("Unexpected error while checking the criteria: " + e.getMessage()); 539 } 540 } 541 542 /** 543 * Pumps all currently-queued messages in the UI thread and then exits. 544 * This is useful to force processing while running tests in the UI thread. 545 */ pumpMessages()546 private void pumpMessages() { 547 class ExitLoopException extends RuntimeException { 548 } 549 550 // Force loop to exit when processing this. Loop.quit() doesn't 551 // work because this is the main Loop. 552 WebkitUtils.onMainThread((Runnable) () -> { 553 throw new ExitLoopException(); // exit loop! 554 }); 555 try { 556 // Pump messages until our message gets through. 557 Looper.loop(); 558 } catch (ExitLoopException e) { 559 } 560 } 561 562 /** 563 * A WebChromeClient used to capture the onProgressChanged for use 564 * in waitFor functions. If a test must override the WebChromeClient, 565 * it can derive from this class or call onProgressChanged 566 * directly. 567 */ 568 public static class WaitForProgressClient extends WebChromeClient { 569 private final WebViewOnUiThread mOnUiThread; 570 WaitForProgressClient(WebViewOnUiThread onUiThread)571 WaitForProgressClient(WebViewOnUiThread onUiThread) { 572 mOnUiThread = onUiThread; 573 } 574 575 @Override 576 @CallSuper onProgressChanged(WebView view, int newProgress)577 public void onProgressChanged(WebView view, int newProgress) { 578 super.onProgressChanged(view, newProgress); 579 mOnUiThread.onProgressChanged(newProgress); 580 } 581 } 582 583 /** 584 * A WebViewClient that captures the onPageFinished for use in 585 * waitFor functions. Using initializeWebView sets the WaitForLoadedClient 586 * into the WebView. If a test needs to set a specific WebViewClient and 587 * needs the waitForCompletion capability then it should derive from 588 * WaitForLoadedClient or call WebViewOnUiThread.onPageFinished. 589 */ 590 public static class WaitForLoadedClient extends WebViewClientCompat { 591 private final WebViewOnUiThread mOnUiThread; 592 WaitForLoadedClient(@onNull WebViewOnUiThread onUiThread)593 public WaitForLoadedClient(@NonNull WebViewOnUiThread onUiThread) { 594 mOnUiThread = onUiThread; 595 } 596 597 @Override 598 @CallSuper onPageFinished(WebView view, String url)599 public void onPageFinished(WebView view, String url) { 600 super.onPageFinished(view, url); 601 mOnUiThread.onPageFinished(); 602 } 603 604 @Override 605 @CallSuper onPageStarted(WebView view, String url, Bitmap favicon)606 public void onPageStarted(WebView view, String url, Bitmap favicon) { 607 super.onPageStarted(view, url, favicon); 608 mOnUiThread.onPageStarted(); 609 } 610 } 611 612 } 613