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