xref: /aosp_15_r20/cts/tests/tests/webkit/src/android/webkit/cts/WebViewTest.java (revision b7c941bb3fa97aba169d73cee0bed2de8ac964bf)
1 /*
2  * Copyright (C) 2009 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 android.webkit.cts;
18 
19 import static org.hamcrest.MatcherAssert.assertThat;
20 import static org.hamcrest.Matchers.greaterThan;
21 import static org.hamcrest.Matchers.lessThan;
22 import static org.junit.Assert.*;
23 
24 import android.app.Activity;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.ContextWrapper;
28 import android.graphics.Bitmap;
29 import android.graphics.Bitmap.Config;
30 import android.graphics.Canvas;
31 import android.graphics.Color;
32 import android.graphics.Picture;
33 import android.graphics.Rect;
34 import android.graphics.pdf.PdfRenderer;
35 import android.net.Uri;
36 import android.os.Bundle;
37 import android.os.CancellationSignal;
38 import android.os.Handler;
39 import android.os.LocaleList;
40 import android.os.Looper;
41 import android.os.Message;
42 import android.os.ParcelFileDescriptor;
43 import android.os.SystemClock;
44 import android.platform.test.annotations.AppModeFull;
45 import android.platform.test.annotations.Presubmit;
46 import android.print.PageRange;
47 import android.print.PrintAttributes;
48 import android.print.PrintDocumentAdapter;
49 import android.print.PrintDocumentAdapter.LayoutResultCallback;
50 import android.print.PrintDocumentAdapter.WriteResultCallback;
51 import android.print.PrintDocumentInfo;
52 import android.util.DisplayMetrics;
53 import android.view.KeyEvent;
54 import android.view.ViewGroup;
55 import android.view.ViewParent;
56 import android.view.textclassifier.TextClassification;
57 import android.view.textclassifier.TextClassifier;
58 import android.view.textclassifier.TextSelection;
59 import android.webkit.CookieSyncManager;
60 import android.webkit.DownloadListener;
61 import android.webkit.JavascriptInterface;
62 import android.webkit.SafeBrowsingResponse;
63 import android.webkit.ValueCallback;
64 import android.webkit.WebBackForwardList;
65 import android.webkit.WebChromeClient;
66 import android.webkit.WebIconDatabase;
67 import android.webkit.WebResourceRequest;
68 import android.webkit.WebSettings;
69 import android.webkit.WebView;
70 import android.webkit.WebView.HitTestResult;
71 import android.webkit.WebView.PictureListener;
72 import android.webkit.WebView.VisualStateCallback;
73 import android.webkit.WebViewClient;
74 import android.webkit.WebViewDatabase;
75 import android.webkit.cts.WebViewSyncLoader.WaitForLoadedClient;
76 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
77 import android.widget.LinearLayout;
78 
79 import androidx.test.core.app.ApplicationProvider;
80 import androidx.test.ext.junit.rules.ActivityScenarioRule;
81 import androidx.test.ext.junit.runners.AndroidJUnit4;
82 import androidx.test.filters.FlakyTest;
83 import androidx.test.filters.MediumTest;
84 
85 import com.android.compatibility.common.util.NullWebViewUtils;
86 import com.android.compatibility.common.util.PollingCheck;
87 
88 import com.google.common.util.concurrent.SettableFuture;
89 
90 import org.junit.After;
91 import org.junit.Assume;
92 import org.junit.Before;
93 import org.junit.Ignore;
94 import org.junit.Rule;
95 import org.junit.Test;
96 import org.junit.runner.RunWith;
97 
98 import java.io.File;
99 import java.io.FileInputStream;
100 import java.io.FileNotFoundException;
101 import java.io.IOException;
102 import java.net.MalformedURLException;
103 import java.net.URL;
104 import java.nio.charset.Charset;
105 import java.nio.charset.StandardCharsets;
106 import java.util.ArrayList;
107 import java.util.Collections;
108 import java.util.HashMap;
109 import java.util.List;
110 import java.util.Map;
111 import java.util.concurrent.Future;
112 import java.util.concurrent.Semaphore;
113 import java.util.concurrent.TimeUnit;
114 import java.util.concurrent.atomic.AtomicBoolean;
115 import java.util.concurrent.atomic.AtomicReference;
116 
117 @AppModeFull
118 @MediumTest
119 @RunWith(AndroidJUnit4.class)
120 public class WebViewTest extends SharedWebViewTest {
121     private static final int INITIAL_PROGRESS = 100;
122     private static final String X_REQUESTED_WITH = "X-Requested-With";
123     private static final String PRINTER_TEST_FILE = "print.pdf";
124     private static final String PDF_PREAMBLE = "%PDF-1";
125     // Snippet of HTML that will prevent favicon requests to the test server.
126     private static final String HTML_HEADER =
127             "<html><head><link rel=\"shortcut icon\" href=\"%23\" /></head>";
128     private static final String SIMPLE_HTML = "<html><body>simple html</body></html>";
129 
130     /**
131      * This is the minimum number of milliseconds to wait for scrolling to start. If no scrolling
132      * has started before this timeout then it is assumed that no scrolling will happen.
133      */
134     private static final long MIN_SCROLL_WAIT_MS = 1000;
135 
136     /**
137      * This is the minimum number of milliseconds to wait for findAll to find all the matches. If
138      * matches are not found, the Listener would call findAll again until it times out.
139      */
140     private static final long MIN_FIND_WAIT_MS = 3000;
141 
142     /**
143      * Once scrolling has started, this is the interval that scrolling is checked to see if there is
144      * a change. If no scrolling change has happened in the given time then it is assumed that
145      * scrolling has stopped.
146      */
147     private static final long SCROLL_WAIT_INTERVAL_MS = 200;
148 
149     @Rule
150     public ActivityScenarioRule mActivityScenarioRule =
151             new ActivityScenarioRule(WebViewCtsActivity.class);
152 
153     private Context mContext;
154     private SharedSdkWebServer mWebServer;
155     private WebIconDatabase mIconDb;
156     private WebView mWebView;
157     private WebViewOnUiThread mOnUiThread;
158 
159     @Before
setUp()160     public void setUp() throws Exception {
161         mWebView = getTestEnvironment().getWebView();
162         mOnUiThread = new WebViewOnUiThread(mWebView);
163         mContext = getTestEnvironment().getContext();
164     }
165 
166     @After
cleanup()167     public void cleanup() throws Exception {
168         if (mOnUiThread != null) {
169             mOnUiThread.cleanUp();
170         }
171         if (mWebServer != null) {
172             mWebServer.shutdown();
173         }
174         if (mIconDb != null) {
175             mIconDb.removeAllIcons();
176             mIconDb.close();
177             mIconDb = null;
178         }
179     }
180 
181     @Override
createTestEnvironment()182     protected SharedWebViewTestEnvironment createTestEnvironment() {
183         Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
184 
185         SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
186 
187         mActivityScenarioRule
188                 .getScenario()
189                 .onActivity(
190                         activity -> {
191                             WebView webView = ((WebViewCtsActivity) activity).getWebView();
192                             builder.setHostAppInvoker(
193                                             SharedWebViewTestEnvironment.createHostAppInvoker(
194                                                     activity))
195                                     .setContext(activity)
196                                     .setWebView(webView)
197                                     .setRootLayout(((WebViewCtsActivity) activity).getRootLayout());
198                         });
199 
200         SharedWebViewTestEnvironment environment = builder.build();
201 
202         // Wait for window focus and clean up the snapshot before
203         // returning the test environment.
204         if (environment.getWebView() != null) {
205             new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
206                 @Override
207                 protected boolean check() {
208                     return ((Activity) environment.getContext()).hasWindowFocus();
209                 }
210             }.run();
211             File f = environment.getContext().getFileStreamPath("snapshot");
212             if (f.exists()) {
213                 f.delete();
214             }
215         }
216 
217         return environment;
218     }
219 
220     @Test
testConstructor()221     public void testConstructor() {
222         WebkitUtils.onMainThreadSync(
223                 () -> {
224                     WebView webView = new WebView(mContext);
225                     webView.destroy();
226                     webView = new WebView(mContext, null);
227                     webView.destroy();
228                     webView = new WebView(mContext, null, 0);
229                     webView.destroy();
230                 });
231     }
232 
233     @Test
testCreatingWebViewWithDeviceEncrpytionFails()234     public void testCreatingWebViewWithDeviceEncrpytionFails() {
235         WebkitUtils.onMainThreadSync(
236                 () -> {
237                     Context deviceEncryptedContext = mContext.createDeviceProtectedStorageContext();
238                     try {
239                         new WebView(deviceEncryptedContext);
240                         fail(
241                                 "WebView should have thrown exception when creating with a device "
242                                         + "protected storage context");
243                     } catch (IllegalArgumentException e) {
244                     }
245                 });
246     }
247 
248     @Test
testCreatingWebViewWithMultipleEncryptionContext()249     public void testCreatingWebViewWithMultipleEncryptionContext() {
250         WebkitUtils.onMainThreadSync(
251                 () -> {
252                     // Credential encryption is the default. Create one here for the sake of
253                     // clarity.
254                     Context credentialEncryptedContext =
255                             mContext.createCredentialProtectedStorageContext();
256                     Context deviceEncryptedContext = mContext.createDeviceProtectedStorageContext();
257 
258                     // No exception should be thrown with credential encryption context.
259                     WebView webView = new WebView(credentialEncryptedContext);
260                     webView.destroy();
261 
262                     try {
263                         new WebView(deviceEncryptedContext);
264                         fail(
265                                 "WebView should have thrown exception when creating with a device "
266                                         + "protected storage context");
267                     } catch (IllegalArgumentException e) {
268                     }
269                 });
270     }
271 
272     @Test
testCreatingWebViewCreatesCookieSyncManager()273     public void testCreatingWebViewCreatesCookieSyncManager() throws Exception {
274         WebkitUtils.onMainThreadSync(
275                 () -> {
276                     WebView webView = new WebView(mContext);
277                     assertNotNull(CookieSyncManager.getInstance());
278                     webView.destroy();
279                 });
280     }
281 
282     @Test
283     // Static methods should be safe to call on non-UI threads
testFindAddress()284     public void testFindAddress() {
285         /*
286          * Info about USPS
287          * http://en.wikipedia.org/wiki/Postal_address#United_States
288          * http://www.usps.com/
289          */
290         // full address
291         assertEquals(
292                 "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826",
293                 WebView.findAddress("455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA 92826"));
294         // Zipcode is optional.
295         assertEquals(
296                 "455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA",
297                 WebView.findAddress("455 LARKSPUR DRIVE CALIFORNIA SPRINGS CALIFORNIA"));
298         // not an address
299         assertNull(WebView.findAddress("This is not an address: no town, no state, no zip."));
300 
301         // would be an address, except for numbers that are not ASCII
302         assertNull(
303                 WebView.findAddress(
304                         "80\uD835\uDFEF \uD835\uDFEF\uD835\uDFEFth Avenue Sunnyvale, CA 94089"));
305     }
306 
307     @Test
testScrollBarOverlay()308     public void testScrollBarOverlay() throws Throwable {
309         WebkitUtils.onMainThreadSync(
310                 () -> {
311                     // These functions have no effect; just verify they don't crash
312                     mWebView.setHorizontalScrollbarOverlay(true);
313                     mWebView.setVerticalScrollbarOverlay(false);
314 
315                     assertTrue(mWebView.overlayHorizontalScrollbar());
316                     assertFalse(mWebView.overlayVerticalScrollbar());
317                 });
318     }
319 
320     @Test
321     @Presubmit
testLoadUrl()322     public void testLoadUrl() throws Exception {
323         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
324 
325         WebkitUtils.onMainThreadSync(
326                 () -> {
327                     assertNull(mWebView.getUrl());
328                     assertNull(mWebView.getOriginalUrl());
329                     assertEquals(INITIAL_PROGRESS, mWebView.getProgress());
330 
331                     String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
332                     mOnUiThread.loadUrlAndWaitForCompletion(url);
333                     assertEquals(100, mWebView.getProgress());
334                     assertEquals(url, mWebView.getUrl());
335                     assertEquals(url, mWebView.getOriginalUrl());
336                     assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, mWebView.getTitle());
337                 });
338     }
339 
340     @Test
testPostUrlWithNonNetworkUrl()341     public void testPostUrlWithNonNetworkUrl() throws Exception {
342         final String nonNetworkUrl = "file:///android_asset/" + TestHtmlConstants.HELLO_WORLD_URL;
343 
344         mOnUiThread.postUrlAndWaitForCompletion(nonNetworkUrl, new byte[1]);
345 
346         WebkitUtils.onMainThreadSync(
347                 () -> {
348                     assertEquals(
349                             "Non-network URL should have loaded",
350                             TestHtmlConstants.HELLO_WORLD_TITLE,
351                             mWebView.getTitle());
352                 });
353     }
354 
355     @Test
testPostUrlWithNetworkUrl()356     public void testPostUrlWithNetworkUrl() throws Exception {
357         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
358 
359         final String networkUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
360         final String postDataString = "username=my_username&password=my_password";
361         final byte[] postData = getTestEnvironment().getEncodingBytes(postDataString, "BASE64");
362 
363         mOnUiThread.postUrlAndWaitForCompletion(networkUrl, postData);
364 
365         HttpRequest request = mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
366         assertEquals("The last request should be POST", request.getMethod(), "POST");
367         assertEquals(request.getBody(), postDataString);
368     }
369 
370     @Test
testLoadUrlDoesNotStripParamsWhenLoadingContentUrls()371     public void testLoadUrlDoesNotStripParamsWhenLoadingContentUrls() throws Exception {
372         WebkitUtils.onMainThreadSync(
373                 () -> {
374                     Uri.Builder uriBuilder =
375                             new Uri.Builder()
376                                     .scheme(ContentResolver.SCHEME_CONTENT)
377                                     .authority(MockContentProvider.AUTHORITY);
378                     uriBuilder.appendPath("foo.html").appendQueryParameter("param", "bar");
379                     String url = uriBuilder.build().toString();
380                     mOnUiThread.loadUrlAndWaitForCompletion(url);
381                     // verify the parameter is not stripped.
382                     Uri uri = Uri.parse(mWebView.getTitle());
383                     assertEquals("bar", uri.getQueryParameter("param"));
384                 });
385     }
386 
387     @Test
testAppInjectedXRequestedWithHeaderIsNotOverwritten()388     public void testAppInjectedXRequestedWithHeaderIsNotOverwritten() throws Exception {
389         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
390 
391         WebkitUtils.onMainThreadSync(
392                 () -> {
393                     String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
394                     HashMap<String, String> map = new HashMap<String, String>();
395                     final String requester = "foo";
396                     map.put(X_REQUESTED_WITH, requester);
397                     mOnUiThread.loadUrlAndWaitForCompletion(url, map);
398 
399                     // verify that the request also includes X-Requested-With header
400                     // but is not overwritten by the webview
401                     HttpRequest request =
402                             mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
403                     String[] matchingHeaders = request.getHeaders(X_REQUESTED_WITH);
404                     assertEquals(1, matchingHeaders.length);
405 
406                     String header = matchingHeaders[0];
407                     assertEquals(requester, header);
408                 });
409     }
410 
411     @Test
testAppCanInjectHeadersViaImmutableMap()412     public void testAppCanInjectHeadersViaImmutableMap() throws Exception {
413         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
414 
415         WebkitUtils.onMainThreadSync(
416                 () -> {
417                     String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
418                     HashMap<String, String> map = new HashMap<String, String>();
419                     final String requester = "foo";
420                     map.put(X_REQUESTED_WITH, requester);
421                     mOnUiThread.loadUrlAndWaitForCompletion(url, Collections.unmodifiableMap(map));
422 
423                     // verify that the request also includes X-Requested-With header
424                     // but is not overwritten by the webview
425                     HttpRequest request =
426                             mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
427                     String[] matchingHeaders = request.getHeaders(X_REQUESTED_WITH);
428                     assertEquals(1, matchingHeaders.length);
429 
430                     String header = matchingHeaders[0];
431                     assertEquals(requester, header);
432                 });
433     }
434 
435     @Test
testCanInjectHeaders()436     public void testCanInjectHeaders() throws Exception {
437         final String X_FOO = "X-foo";
438         final String X_FOO_VALUE = "test";
439 
440         final String X_REFERER = "Referer";
441         final String X_REFERER_VALUE = "http://www.example.com/";
442         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
443         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
444         HashMap<String, String> map = new HashMap<String, String>();
445         map.put(X_FOO, X_FOO_VALUE);
446         map.put(X_REFERER, X_REFERER_VALUE);
447         mOnUiThread.loadUrlAndWaitForCompletion(url, map);
448 
449         HttpRequest request = mWebServer.getLastAssetRequest(TestHtmlConstants.HELLO_WORLD_URL);
450         for (Map.Entry<String, String> value : map.entrySet()) {
451             String header = value.getKey();
452             String[] matchingHeaders = request.getHeaders(header);
453             assertEquals("header " + header + " not found", 1, matchingHeaders.length);
454             assertEquals(value.getValue(), matchingHeaders[0]);
455         }
456     }
457 
458     @Test
459     @SuppressWarnings("deprecation")
testGetVisibleTitleHeight()460     public void testGetVisibleTitleHeight() throws Exception {
461         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
462 
463         WebkitUtils.onMainThreadSync(
464                 () -> {
465                     String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
466                     mOnUiThread.loadUrlAndWaitForCompletion(url);
467                     assertEquals(0, mWebView.getVisibleTitleHeight());
468                 });
469     }
470 
471     @Test
testGetOriginalUrl()472     public void testGetOriginalUrl() throws Throwable {
473         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
474 
475         WebkitUtils.onMainThreadSync(
476                 () -> {
477                     final String finalUrl =
478                             mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
479                     final String redirectUrl =
480                             mWebServer.getRedirectingAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
481 
482                     assertNull(mWebView.getUrl());
483                     assertNull(mWebView.getOriginalUrl());
484 
485                     // By default, WebView sends an intent to ask the system to
486                     // handle loading a new URL. We set a WebViewClient as
487                     // WebViewClient.shouldOverrideUrlLoading() returns false, so
488                     // the WebView will load the new URL.
489                     mWebView.setWebViewClient(new WaitForLoadedClient(mOnUiThread));
490                     mOnUiThread.loadUrlAndWaitForCompletion(redirectUrl);
491 
492                     assertEquals(finalUrl, mWebView.getUrl());
493                     assertEquals(redirectUrl, mWebView.getOriginalUrl());
494                 });
495     }
496 
497     @Test
testStopLoading()498     public void testStopLoading() throws Exception {
499         assertEquals(INITIAL_PROGRESS, mOnUiThread.getProgress());
500 
501         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
502         String url = mWebServer.getDelayedAssetUrl(TestHtmlConstants.STOP_LOADING_URL);
503 
504         class JsInterface {
505             private boolean mPageLoaded;
506 
507             @JavascriptInterface
508             public synchronized void pageLoaded() {
509                 mPageLoaded = true;
510                 notify();
511             }
512 
513             public synchronized boolean getPageLoaded() {
514                 return mPageLoaded;
515             }
516         }
517 
518         JsInterface jsInterface = new JsInterface();
519 
520         mOnUiThread.getSettings().setJavaScriptEnabled(true);
521         mOnUiThread.addJavascriptInterface(jsInterface, "javabridge");
522         mOnUiThread.loadUrl(url);
523         mOnUiThread.stopLoading();
524 
525         // We wait to see that the onload callback in the HTML is not fired.
526         synchronized (jsInterface) {
527             jsInterface.wait(3000);
528         }
529 
530         assertFalse(jsInterface.getPageLoaded());
531     }
532 
533     @Test
testGoBackAndForward()534     public void testGoBackAndForward() throws Exception {
535         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
536 
537         assertGoBackOrForwardBySteps(false, -1);
538         assertGoBackOrForwardBySteps(false, 1);
539 
540         String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
541         String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
542         String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
543 
544         mOnUiThread.loadUrlAndWaitForCompletion(url1);
545         pollingCheckWebBackForwardList(url1, 0, 1);
546         assertGoBackOrForwardBySteps(false, -1);
547         assertGoBackOrForwardBySteps(false, 1);
548 
549         mOnUiThread.loadUrlAndWaitForCompletion(url2);
550         pollingCheckWebBackForwardList(url2, 1, 2);
551         assertGoBackOrForwardBySteps(true, -1);
552         assertGoBackOrForwardBySteps(false, 1);
553 
554         mOnUiThread.loadUrlAndWaitForCompletion(url3);
555         pollingCheckWebBackForwardList(url3, 2, 3);
556         assertGoBackOrForwardBySteps(true, -2);
557         assertGoBackOrForwardBySteps(false, 1);
558 
559         mOnUiThread.goBack();
560         pollingCheckWebBackForwardList(url2, 1, 3);
561         assertGoBackOrForwardBySteps(true, -1);
562         assertGoBackOrForwardBySteps(true, 1);
563 
564         mOnUiThread.goForward();
565         pollingCheckWebBackForwardList(url3, 2, 3);
566         assertGoBackOrForwardBySteps(true, -2);
567         assertGoBackOrForwardBySteps(false, 1);
568 
569         mOnUiThread.goBackOrForward(-2);
570         pollingCheckWebBackForwardList(url1, 0, 3);
571         assertGoBackOrForwardBySteps(false, -1);
572         assertGoBackOrForwardBySteps(true, 2);
573 
574         mOnUiThread.goBackOrForward(2);
575         pollingCheckWebBackForwardList(url3, 2, 3);
576         assertGoBackOrForwardBySteps(true, -2);
577         assertGoBackOrForwardBySteps(false, 1);
578     }
579 
580     @Test
testAddJavascriptInterface()581     public void testAddJavascriptInterface() throws Exception {
582         mOnUiThread.getSettings().setJavaScriptEnabled(true);
583         mOnUiThread.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
584 
585         final class TestJavaScriptInterface {
586             private boolean mWasProvideResultCalled;
587             private String mResult;
588 
589             private synchronized String waitForResult() {
590                 while (!mWasProvideResultCalled) {
591                     try {
592                         wait(WebkitUtils.TEST_TIMEOUT_MS);
593                     } catch (InterruptedException e) {
594                         continue;
595                     }
596                     if (!mWasProvideResultCalled) {
597                         fail("Unexpected timeout");
598                     }
599                 }
600                 return mResult;
601             }
602 
603             public synchronized boolean wasProvideResultCalled() {
604                 return mWasProvideResultCalled;
605             }
606 
607             @JavascriptInterface
608             public synchronized void provideResult(String result) {
609                 mWasProvideResultCalled = true;
610                 mResult = result;
611                 notify();
612             }
613         }
614 
615         final TestJavaScriptInterface obj = new TestJavaScriptInterface();
616         mOnUiThread.addJavascriptInterface(obj, "interface");
617         assertFalse(obj.wasProvideResultCalled());
618 
619         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
620         String url = mWebServer.getAssetUrl(TestHtmlConstants.ADD_JAVA_SCRIPT_INTERFACE_URL);
621         mOnUiThread.loadUrlAndWaitForCompletion(url);
622         assertEquals("Original title", obj.waitForResult());
623 
624         // Verify that only methods annotated with @JavascriptInterface are exposed
625         // on the JavaScript interface object.
626         assertEquals(
627                 "\"function\"",
628                 mOnUiThread.evaluateJavascriptSync("typeof interface.provideResult"));
629 
630         assertEquals(
631                 "\"undefined\"",
632                 mOnUiThread.evaluateJavascriptSync("typeof interface.wasProvideResultCalled"));
633 
634         assertEquals(
635                 "\"undefined\"", mOnUiThread.evaluateJavascriptSync("typeof interface.getClass"));
636     }
637 
638     @Test
testAddJavascriptInterfaceNullObject()639     public void testAddJavascriptInterfaceNullObject() throws Exception {
640         mOnUiThread.getSettings().setJavaScriptEnabled(true);
641         String setTitleToPropertyTypeHtml =
642                 "<html><head></head><body onload=\"document.title = typeof"
643                         + " window.injectedObject;\"></body></html>";
644 
645         // Test that the property is initially undefined.
646         mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
647         assertEquals("undefined", mOnUiThread.getTitle());
648 
649         // Test that adding a null object has no effect.
650         mOnUiThread.addJavascriptInterface(null, "injectedObject");
651         mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
652         assertEquals("undefined", mOnUiThread.getTitle());
653 
654         // Test that adding an object gives an object type.
655         final Object obj = new Object();
656         mOnUiThread.addJavascriptInterface(obj, "injectedObject");
657         mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
658         assertEquals("object", mOnUiThread.getTitle());
659 
660         // Test that trying to replace with a null object has no effect.
661         mOnUiThread.addJavascriptInterface(null, "injectedObject");
662         mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
663         assertEquals("object", mOnUiThread.getTitle());
664     }
665 
666     @Test
testRemoveJavascriptInterface()667     public void testRemoveJavascriptInterface() throws Exception {
668         mOnUiThread.getSettings().setJavaScriptEnabled(true);
669         String setTitleToPropertyTypeHtml =
670                 "<html><head></head><body onload=\"document.title = typeof"
671                         + " window.injectedObject;\"></body></html>";
672 
673         // Test that adding an object gives an object type.
674         mOnUiThread.addJavascriptInterface(new Object(), "injectedObject");
675         mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
676         assertEquals("object", mOnUiThread.getTitle());
677 
678         // Test that reloading the page after removing the object leaves the property undefined.
679         mOnUiThread.removeJavascriptInterface("injectedObject");
680         mOnUiThread.loadDataAndWaitForCompletion(setTitleToPropertyTypeHtml, "text/html", null);
681         assertEquals("undefined", mOnUiThread.getTitle());
682     }
683 
684     @Test
testUseRemovedJavascriptInterface()685     public void testUseRemovedJavascriptInterface() throws Throwable {
686         class RemovedObject {
687             @Override
688             @JavascriptInterface
689             public String toString() {
690                 return "removedObject";
691             }
692 
693             @JavascriptInterface
694             public void remove() throws Throwable {
695                 mOnUiThread.removeJavascriptInterface("removedObject");
696                 System.gc();
697             }
698         }
699         class ResultObject {
700             private String mResult;
701             private boolean mIsResultAvailable;
702 
703             @JavascriptInterface
704             public synchronized void setResult(String result) {
705                 mResult = result;
706                 mIsResultAvailable = true;
707                 notify();
708             }
709 
710             public synchronized String getResult() {
711                 while (!mIsResultAvailable) {
712                     try {
713                         wait();
714                     } catch (InterruptedException e) {
715                     }
716                 }
717                 return mResult;
718             }
719         }
720         final ResultObject resultObject = new ResultObject();
721 
722         // Test that an object is still usable if removed while the page is in use, even if we have
723         // no external references to it.
724         mOnUiThread.getSettings().setJavaScriptEnabled(true);
725         mOnUiThread.addJavascriptInterface(new RemovedObject(), "removedObject");
726         mOnUiThread.addJavascriptInterface(resultObject, "resultObject");
727         mOnUiThread.loadDataAndWaitForCompletion(
728                 "<html><head></head>"
729                         + "<body onload=\"window.removedObject.remove();"
730                         + "resultObject.setResult(removedObject.toString());\"></body></html>",
731                 "text/html",
732                 null);
733         assertEquals("removedObject", resultObject.getResult());
734     }
735 
736     @Test
testAddJavascriptInterfaceExceptions()737     public void testAddJavascriptInterfaceExceptions() throws Exception {
738         WebSettings settings = mOnUiThread.getSettings();
739         settings.setJavaScriptEnabled(true);
740         settings.setJavaScriptCanOpenWindowsAutomatically(true);
741 
742         final AtomicBoolean mJsInterfaceWasCalled =
743                 new AtomicBoolean(false) {
744                     @JavascriptInterface
745                     public synchronized void call() {
746                         set(true);
747                         // The main purpose of this test is to ensure an exception here does not
748                         // crash the implementation.
749                         throw new RuntimeException("Javascript Interface exception");
750                     }
751                 };
752 
753         mOnUiThread.addJavascriptInterface(mJsInterfaceWasCalled, "interface");
754 
755         mOnUiThread.loadUrlAndWaitForCompletion("about:blank");
756 
757         assertFalse(mJsInterfaceWasCalled.get());
758 
759         assertEquals(
760                 "\"pass\"",
761                 mOnUiThread.evaluateJavascriptSync(
762                         "try {interface.call(); 'fail'; } catch (exception) { 'pass'; } "));
763         assertTrue(mJsInterfaceWasCalled.get());
764     }
765 
766     @Test
testJavascriptInterfaceCustomPropertiesClearedOnReload()767     public void testJavascriptInterfaceCustomPropertiesClearedOnReload() throws Exception {
768         mOnUiThread.getSettings().setJavaScriptEnabled(true);
769 
770         mOnUiThread.addJavascriptInterface(new Object(), "interface");
771         mOnUiThread.loadUrlAndWaitForCompletion("about:blank");
772 
773         assertEquals("42", mOnUiThread.evaluateJavascriptSync("interface.custom_property = 42"));
774 
775         assertEquals("true", mOnUiThread.evaluateJavascriptSync("'custom_property' in interface"));
776 
777         mOnUiThread.reloadAndWaitForCompletion();
778 
779         assertEquals("false", mOnUiThread.evaluateJavascriptSync("'custom_property' in interface"));
780     }
781 
782     @Test
783     @FlakyTest(bugId = 171702662)
testJavascriptInterfaceForClientPopup()784     public void testJavascriptInterfaceForClientPopup() throws Exception {
785         mOnUiThread.getSettings().setJavaScriptEnabled(true);
786         mOnUiThread.getSettings().setJavaScriptCanOpenWindowsAutomatically(true);
787         mOnUiThread.getSettings().setSupportMultipleWindows(true);
788 
789         class TestJavaScriptInterface {
790             @JavascriptInterface
791             public int test() {
792                 return 42;
793             }
794         }
795         final TestJavaScriptInterface obj = new TestJavaScriptInterface();
796 
797         final WebView childWebView = mOnUiThread.createWebView();
798         try {
799             WebViewOnUiThread childOnUiThread = new WebViewOnUiThread(childWebView);
800             childOnUiThread.getSettings().setJavaScriptEnabled(true);
801             childOnUiThread.addJavascriptInterface(obj, "interface");
802 
803             final SettableFuture<Void> onCreateWindowFuture = SettableFuture.create();
804             mOnUiThread.setWebChromeClient(
805                     new WebViewSyncLoader.WaitForProgressClient(mOnUiThread) {
806                         @Override
807                         public boolean onCreateWindow(
808                                 WebView view,
809                                 boolean isDialog,
810                                 boolean isUserGesture,
811                                 Message resultMsg) {
812                             getTestEnvironment().addContentView(
813                                     childWebView,
814                                     new ViewGroup.LayoutParams(
815                                             ViewGroup.LayoutParams.FILL_PARENT,
816                                             ViewGroup.LayoutParams.WRAP_CONTENT));
817                             WebView.WebViewTransport transport =
818                                     (WebView.WebViewTransport) resultMsg.obj;
819                             transport.setWebView(childWebView);
820                             resultMsg.sendToTarget();
821                             onCreateWindowFuture.set(null);
822                             return true;
823                         }
824                     });
825 
826             mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
827             mOnUiThread.loadUrlAndWaitForCompletion(
828                     mWebServer.getAssetUrl(TestHtmlConstants.POPUP_URL));
829             WebkitUtils.waitForFuture(onCreateWindowFuture);
830 
831             childOnUiThread.loadUrlAndWaitForCompletion("about:blank");
832 
833             assertEquals("true", childOnUiThread.evaluateJavascriptSync("'interface' in window"));
834 
835             assertEquals(
836                     "The injected object should be functional",
837                     "42",
838                     childOnUiThread.evaluateJavascriptSync("interface.test()"));
839         } finally {
840             WebkitUtils.onMainThreadSync(() -> {
841                 ViewParent parent = childWebView.getParent();
842                 if (parent instanceof ViewGroup) {
843                     ((ViewGroup) parent).removeView(childWebView);
844                 }
845                 childWebView.destroy();
846             });
847         }
848     }
849 
850     private final class TestPictureListener implements PictureListener {
851         public int callCount;
852 
853         @Override
onNewPicture(WebView view, Picture picture)854         public void onNewPicture(WebView view, Picture picture) {
855             // Need to inform the listener tracking new picture
856             // for the "page loaded" knowledge since it has been replaced.
857             mOnUiThread.onNewPicture();
858             this.callCount += 1;
859         }
860     }
861 
waitForPictureToHaveColor(int color, final TestPictureListener listener)862     private Picture waitForPictureToHaveColor(int color, final TestPictureListener listener)
863             throws Throwable {
864         final int MAX_ON_NEW_PICTURE_ITERATIONS = 5;
865         final AtomicReference<Picture> pictureRef = new AtomicReference<Picture>();
866         for (int i = 0; i < MAX_ON_NEW_PICTURE_ITERATIONS; i++) {
867             final int oldCallCount = listener.callCount;
868             WebkitUtils.onMainThreadSync(
869                     () -> {
870                         pictureRef.set(mWebView.capturePicture());
871                     });
872             if (isPictureFilledWithColor(pictureRef.get(), color)) break;
873             new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
874                 @Override
875                 protected boolean check() {
876                     return listener.callCount > oldCallCount;
877                 }
878             }.run();
879         }
880         return pictureRef.get();
881     }
882 
883     @Test
testCapturePicture()884     public void testCapturePicture() throws Exception, Throwable {
885         final TestPictureListener listener = new TestPictureListener();
886 
887         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
888         final String url = mWebServer.getAssetUrl(TestHtmlConstants.BLANK_PAGE_URL);
889         mOnUiThread.setPictureListener(listener);
890         // Showing the blank page will fill the picture with the background color.
891         mOnUiThread.loadUrlAndWaitForCompletion(url);
892         // The default background color is white.
893         Picture oldPicture = waitForPictureToHaveColor(Color.WHITE, listener);
894 
895         WebkitUtils.onMainThread(
896                 () -> {
897                     mWebView.setBackgroundColor(Color.CYAN);
898                 });
899         mOnUiThread.reloadAndWaitForCompletion();
900         waitForPictureToHaveColor(Color.CYAN, listener);
901 
902         assertTrue(
903                 "The content of the previously captured picture should not update automatically",
904                 isPictureFilledWithColor(oldPicture, Color.WHITE));
905     }
906 
907     @Test
testSetPictureListener()908     public void testSetPictureListener() throws Exception, Throwable {
909         final class MyPictureListener implements PictureListener {
910             public int callCount;
911             public WebView webView;
912             public Picture picture;
913 
914             @Override
915             public void onNewPicture(WebView view, Picture picture) {
916                 // Need to inform the listener tracking new picture
917                 // for the "page loaded" knowledge since it has been replaced.
918                 mOnUiThread.onNewPicture();
919                 this.callCount += 1;
920                 this.webView = view;
921                 this.picture = picture;
922             }
923         }
924 
925         final MyPictureListener listener = new MyPictureListener();
926         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
927         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
928         mOnUiThread.setPictureListener(listener);
929         mOnUiThread.loadUrlAndWaitForCompletion(url);
930         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
931             @Override
932             protected boolean check() {
933                 return listener.callCount > 0;
934             }
935         }.run();
936         assertEquals(mWebView, listener.webView);
937         assertNull(listener.picture);
938 
939         final int oldCallCount = listener.callCount;
940         final String newUrl = mWebServer.getAssetUrl(TestHtmlConstants.SMALL_IMG_URL);
941         mOnUiThread.loadUrlAndWaitForCompletion(newUrl);
942         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
943             @Override
944             protected boolean check() {
945                 return listener.callCount > oldCallCount;
946             }
947         }.run();
948     }
949 
950     @Test
testAccessHttpAuthUsernamePassword()951     public void testAccessHttpAuthUsernamePassword() {
952         WebkitUtils.onMainThreadSync(
953                 () -> {
954                     try {
955                         WebViewDatabase.getInstance(mContext).clearHttpAuthUsernamePassword();
956 
957                         String host = "http://localhost:8080";
958                         String realm = "testrealm";
959                         String userName = "user";
960                         String password = "password";
961 
962                         String[] result = mWebView.getHttpAuthUsernamePassword(host, realm);
963                         assertNull(result);
964 
965                         mWebView.setHttpAuthUsernamePassword(host, realm, userName, password);
966                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
967                         assertNotNull(result);
968                         assertEquals(userName, result[0]);
969                         assertEquals(password, result[1]);
970 
971                         String newPassword = "newpassword";
972                         mWebView.setHttpAuthUsernamePassword(host, realm, userName, newPassword);
973                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
974                         assertNotNull(result);
975                         assertEquals(userName, result[0]);
976                         assertEquals(newPassword, result[1]);
977 
978                         String newUserName = "newuser";
979                         mWebView.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
980                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
981                         assertNotNull(result);
982                         assertEquals(newUserName, result[0]);
983                         assertEquals(newPassword, result[1]);
984 
985                         // the user is set to null, can not change any thing in the future
986                         mWebView.setHttpAuthUsernamePassword(host, realm, null, password);
987                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
988                         assertNotNull(result);
989                         assertNull(result[0]);
990                         assertEquals(password, result[1]);
991 
992                         mWebView.setHttpAuthUsernamePassword(host, realm, userName, null);
993                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
994                         assertNotNull(result);
995                         assertEquals(userName, result[0]);
996                         assertNull(result[1]);
997 
998                         mWebView.setHttpAuthUsernamePassword(host, realm, null, null);
999                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
1000                         assertNotNull(result);
1001                         assertNull(result[0]);
1002                         assertNull(result[1]);
1003 
1004                         mWebView.setHttpAuthUsernamePassword(host, realm, newUserName, newPassword);
1005                         result = mWebView.getHttpAuthUsernamePassword(host, realm);
1006                         assertNotNull(result);
1007                         assertEquals(newUserName, result[0]);
1008                         assertEquals(newPassword, result[1]);
1009                     } finally {
1010                         WebViewDatabase.getInstance(mContext).clearHttpAuthUsernamePassword();
1011                     }
1012                 });
1013     }
1014 
1015     @Test
testWebViewDatabaseAccessHttpAuthUsernamePassword()1016     public void testWebViewDatabaseAccessHttpAuthUsernamePassword() {
1017         WebkitUtils.onMainThreadSync(
1018                 () -> {
1019                     WebViewDatabase webViewDb = WebViewDatabase.getInstance(mContext);
1020                     try {
1021                         webViewDb.clearHttpAuthUsernamePassword();
1022 
1023                         String host = "http://localhost:8080";
1024                         String realm = "testrealm";
1025                         String userName = "user";
1026                         String password = "password";
1027 
1028                         String[] result = mWebView.getHttpAuthUsernamePassword(host, realm);
1029                         assertNull(result);
1030 
1031                         webViewDb.setHttpAuthUsernamePassword(host, realm, userName, password);
1032                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1033                         assertNotNull(result);
1034                         assertEquals(userName, result[0]);
1035                         assertEquals(password, result[1]);
1036 
1037                         String newPassword = "newpassword";
1038                         webViewDb.setHttpAuthUsernamePassword(host, realm, userName, newPassword);
1039                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1040                         assertNotNull(result);
1041                         assertEquals(userName, result[0]);
1042                         assertEquals(newPassword, result[1]);
1043 
1044                         String newUserName = "newuser";
1045                         webViewDb.setHttpAuthUsernamePassword(
1046                                 host, realm, newUserName, newPassword);
1047                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1048                         assertNotNull(result);
1049                         assertEquals(newUserName, result[0]);
1050                         assertEquals(newPassword, result[1]);
1051 
1052                         // the user is set to null, can not change any thing in the future
1053                         webViewDb.setHttpAuthUsernamePassword(host, realm, null, password);
1054                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1055                         assertNotNull(result);
1056                         assertNull(result[0]);
1057                         assertEquals(password, result[1]);
1058 
1059                         webViewDb.setHttpAuthUsernamePassword(host, realm, userName, null);
1060                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1061                         assertNotNull(result);
1062                         assertEquals(userName, result[0]);
1063                         assertNull(result[1]);
1064 
1065                         webViewDb.setHttpAuthUsernamePassword(host, realm, null, null);
1066                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1067                         assertNotNull(result);
1068                         assertNull(result[0]);
1069                         assertNull(result[1]);
1070 
1071                         webViewDb.setHttpAuthUsernamePassword(
1072                                 host, realm, newUserName, newPassword);
1073                         result = webViewDb.getHttpAuthUsernamePassword(host, realm);
1074                         assertNotNull(result);
1075                         assertEquals(newUserName, result[0]);
1076                         assertEquals(newPassword, result[1]);
1077                     } finally {
1078                         webViewDb.clearHttpAuthUsernamePassword();
1079                     }
1080                 });
1081     }
1082 
1083     @Test
testLoadData()1084     public void testLoadData() throws Throwable {
1085         final String firstTitle = "Hello, World!";
1086         final String HTML_CONTENT =
1087                 "<html><head><title>" + firstTitle + "</title></head><body></body>" + "</html>";
1088         mOnUiThread.loadDataAndWaitForCompletion(HTML_CONTENT, "text/html", null);
1089         assertEquals(firstTitle, mOnUiThread.getTitle());
1090 
1091         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1092         final String crossOriginUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
1093         mOnUiThread.getSettings().setJavaScriptEnabled(true);
1094         final String secondTitle = "Foo bar";
1095         mOnUiThread.loadDataAndWaitForCompletion(
1096                 "<html><head><title>"
1097                         + secondTitle
1098                         + "</title></head><body onload=\""
1099                         + "document.title = "
1100                         + "document.getElementById('frame').contentWindow.location.href;"
1101                         + "\"><iframe id=\"frame\" src=\""
1102                         + crossOriginUrl
1103                         + "\"></body></html>",
1104                 "text/html",
1105                 null);
1106         assertEquals(
1107                 "Page title should not change, because it should be an error to access a "
1108                         + "cross-site frame's href.",
1109                 secondTitle,
1110                 mOnUiThread.getTitle());
1111     }
1112 
1113     @Test
testLoadDataWithBaseUrl_resolvesRelativeToBaseUrl()1114     public void testLoadDataWithBaseUrl_resolvesRelativeToBaseUrl() throws Throwable {
1115         assertNull(mOnUiThread.getUrl());
1116         String imgUrl = TestHtmlConstants.SMALL_IMG_URL; // relative
1117 
1118         // Trying to resolve a relative URL against a data URL without a base URL
1119         // will fail and we won't make a request to the test web server.
1120         // By using the test web server as the base URL we expect to see a request
1121         // for the relative URL in the test server.
1122         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1123         final String baseUrl = mWebServer.getAssetUrl("foo.html");
1124         mWebServer.resetRequestState();
1125         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1126                 baseUrl,
1127                 HTML_HEADER + "<body><img src=\"" + imgUrl + "\"/></body></html>",
1128                 "text/html",
1129                 "UTF-8",
1130                 null);
1131         assertTrue(
1132                 "The resource request should make it to the server",
1133                 mWebServer.wasResourceRequested(imgUrl));
1134     }
1135 
1136     @Test
testLoadDataWithBaseUrl_historyUrl()1137     public void testLoadDataWithBaseUrl_historyUrl() throws Throwable {
1138         final String baseUrl = "http://www.baseurl.com/";
1139         final String historyUrl = "http://www.example.com/";
1140         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1141                 baseUrl, SIMPLE_HTML, "text/html", "UTF-8", historyUrl);
1142         assertEquals(historyUrl, mOnUiThread.getUrl());
1143     }
1144 
1145     @Test
testLoadDataWithBaseUrl_nullHistoryUrlShowsAsAboutBlank()1146     public void testLoadDataWithBaseUrl_nullHistoryUrlShowsAsAboutBlank() throws Throwable {
1147         // Check that reported URL is "about:blank" when supplied history URL
1148         // is null.
1149         final String baseUrl = "http://www.baseurl.com/";
1150         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1151                 baseUrl, SIMPLE_HTML, "text/html", "UTF-8", null);
1152         assertEquals("about:blank", mOnUiThread.getUrl());
1153     }
1154 
1155     @Test
testLoadDataWithBaseUrl_javascriptCanAccessOrigin()1156     public void testLoadDataWithBaseUrl_javascriptCanAccessOrigin() throws Throwable {
1157         // Test that JavaScript can access content from the same origin as the base URL.
1158         mOnUiThread.getSettings().setJavaScriptEnabled(true);
1159         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1160         final String baseUrl = mWebServer.getAssetUrl("foo.html");
1161         final String crossOriginUrl = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
1162         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1163                 baseUrl,
1164                 HTML_HEADER
1165                         + "<body onload=\"document.title ="
1166                         + " document.getElementById('frame').contentWindow.location.href;\"><iframe"
1167                         + " id=\"frame\" src=\""
1168                         + crossOriginUrl
1169                         + "\"></body></html>",
1170                 "text/html",
1171                 "UTF-8",
1172                 null);
1173         assertEquals(crossOriginUrl, mOnUiThread.getTitle());
1174     }
1175 
1176     @Test
testLoadDataWithBaseUrl_dataBaseUrlIgnoresHistoryUrl()1177     public void testLoadDataWithBaseUrl_dataBaseUrlIgnoresHistoryUrl() throws Throwable {
1178         // Check that when the base URL uses the 'data' scheme, a 'data' scheme URL is used and the
1179         // history URL is ignored.
1180         final String baseUrl = "data:foo";
1181         final String historyUrl = "http://www.example.com/";
1182         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1183                 baseUrl, SIMPLE_HTML, "text/html", "UTF-8", historyUrl);
1184 
1185         final String currentUrl = mOnUiThread.getUrl();
1186         assertEquals(
1187                 "Current URL (" + currentUrl + ") should be a data URI",
1188                 0,
1189                 mOnUiThread.getUrl().indexOf("data:text/html"));
1190         assertThat(
1191                 "Current URL (" + currentUrl + ") should contain the simple HTML we loaded",
1192                 mOnUiThread.getUrl().indexOf("simple html"),
1193                 greaterThan(0));
1194     }
1195 
1196     @Test
testLoadDataWithBaseUrl_unencodedContentHttpBaseUrl()1197     public void testLoadDataWithBaseUrl_unencodedContentHttpBaseUrl() throws Throwable {
1198         // Check that when a non-data: base URL is used, we treat the String to load as
1199         // a raw string and just dump it into the WebView, i.e. not decoding any URL entities.
1200         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1201                 "http://www.foo.com",
1202                 HTML_HEADER + "<title>Hello World%21</title><body>bar</body></html>",
1203                 "text/html",
1204                 "UTF-8",
1205                 null);
1206         assertEquals("Hello World%21", mOnUiThread.getTitle());
1207     }
1208 
1209     @Test
testLoadDataWithBaseUrl_urlEncodedContentDataBaseUrl()1210     public void testLoadDataWithBaseUrl_urlEncodedContentDataBaseUrl() throws Throwable {
1211         // Check that when a data: base URL is used, we treat the String to load as a data: URL
1212         // and run load steps such as decoding URL entities (i.e., contrary to the test case
1213         // above.)
1214         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1215                 "data:foo",
1216                 HTML_HEADER + "<title>Hello World%21</title></html>",
1217                 "text/html",
1218                 "UTF-8",
1219                 null);
1220         assertEquals("Hello World!", mOnUiThread.getTitle());
1221     }
1222 
1223     @Test
testLoadDataWithBaseUrl_nullSafe()1224     public void testLoadDataWithBaseUrl_nullSafe() throws Throwable {
1225         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(null, null, null, null, null);
1226         assertEquals("about:blank", mOnUiThread.getUrl());
1227     }
1228 
deleteIfExists(File file)1229     private void deleteIfExists(File file) throws IOException {
1230         if (file.exists()) {
1231             file.delete();
1232         }
1233     }
1234 
readTextFile(File file, Charset encoding)1235     private String readTextFile(File file, Charset encoding)
1236             throws FileNotFoundException, IOException {
1237         FileInputStream stream = new FileInputStream(file);
1238         byte[] bytes = new byte[(int) file.length()];
1239         stream.read(bytes);
1240         stream.close();
1241         return new String(bytes, encoding);
1242     }
1243 
doSaveWebArchive(String baseName, boolean autoName, final String expectName)1244     private void doSaveWebArchive(String baseName, boolean autoName, final String expectName)
1245             throws Throwable {
1246         final Semaphore saving = new Semaphore(0);
1247         ValueCallback<String> callback =
1248                 new ValueCallback<String>() {
1249                     @Override
1250                     public void onReceiveValue(String savedName) {
1251                         assertEquals(expectName, savedName);
1252                         saving.release();
1253                     }
1254                 };
1255 
1256         mOnUiThread.saveWebArchive(baseName, autoName, callback);
1257         assertTrue(saving.tryAcquire(WebkitUtils.TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS));
1258     }
1259 
1260     @Test
testSaveWebArchive()1261     public void testSaveWebArchive() throws Throwable {
1262         final String testPage = "testSaveWebArchive test page";
1263 
1264         File dir = mContext.getFilesDir();
1265         String dirStr = dir.toString();
1266 
1267         File test = new File(dir, "test.mht");
1268         deleteIfExists(test);
1269         String testStr = test.getAbsolutePath();
1270 
1271         File index = new File(dir, "index.mht");
1272         deleteIfExists(index);
1273         String indexStr = index.getAbsolutePath();
1274 
1275         File index1 = new File(dir, "index-1.mht");
1276         deleteIfExists(index1);
1277         String index1Str = index1.getAbsolutePath();
1278 
1279         mOnUiThread.loadDataAndWaitForCompletion(testPage, "text/html", "UTF-8");
1280 
1281         try {
1282             // Save test.mht
1283             doSaveWebArchive(testStr, false, testStr);
1284 
1285             // Check the contents of test.mht
1286             String testMhtml = readTextFile(test, StandardCharsets.UTF_8);
1287             assertTrue(testMhtml.contains(testPage));
1288 
1289             // Save index.mht
1290             doSaveWebArchive(dirStr + "/", true, indexStr);
1291 
1292             // Check the contents of index.mht
1293             String indexMhtml = readTextFile(index, StandardCharsets.UTF_8);
1294             assertTrue(indexMhtml.contains(testPage));
1295 
1296             // Save index-1.mht since index.mht already exists
1297             doSaveWebArchive(dirStr + "/", true, index1Str);
1298 
1299             // Check the contents of index-1.mht
1300             String index1Mhtml = readTextFile(index1, StandardCharsets.UTF_8);
1301             assertTrue(index1Mhtml.contains(testPage));
1302 
1303             // Try a file in a bogus directory
1304             doSaveWebArchive("/bogus/path/test.mht", false, null);
1305 
1306             // Try a bogus directory
1307             doSaveWebArchive("/bogus/path/", true, null);
1308         } finally {
1309             deleteIfExists(test);
1310             deleteIfExists(index);
1311             deleteIfExists(index1);
1312         }
1313     }
1314 
1315     private static class WaitForFindResultsListener implements WebView.FindListener {
1316         private final SettableFuture<Integer> mFuture;
1317         private final WebView mWebView;
1318         private final int mMatchesWanted;
1319         private final String mStringWanted;
1320         private final boolean mRetry;
1321 
WaitForFindResultsListener(WebView wv, String wanted, int matches, boolean retry)1322         WaitForFindResultsListener(WebView wv, String wanted, int matches, boolean retry) {
1323             mFuture = SettableFuture.create();
1324             mWebView = wv;
1325             mMatchesWanted = matches;
1326             mStringWanted = wanted;
1327             mRetry = retry;
1328         }
1329 
future()1330         public Future<Integer> future() {
1331             return mFuture;
1332         }
1333 
1334         @Override
onFindResultReceived( int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting)1335         public void onFindResultReceived(
1336                 int activeMatchOrdinal, int numberOfMatches, boolean isDoneCounting) {
1337             try {
1338                 assertEquals(
1339                         "WebView.FindListener callbacks should occur on the UI thread",
1340                         Looper.myLooper(),
1341                         Looper.getMainLooper());
1342             } catch (Throwable t) {
1343                 mFuture.setException(t);
1344             }
1345             if (isDoneCounting) {
1346                 // If mRetry set to true and matches aren't equal, call findAll again
1347                 if (mRetry && numberOfMatches != mMatchesWanted) {
1348                     mWebView.findAll(mStringWanted);
1349                 } else {
1350                     mFuture.set(numberOfMatches);
1351                 }
1352             }
1353         }
1354     }
1355 
1356     @Test
testFindAll()1357     public void testFindAll() throws Throwable {
1358         // Make the page scrollable, so we can detect the scrolling to make sure the
1359         // content fully loaded.
1360         mOnUiThread.setInitialScale(100);
1361         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
1362         int dimension = Math.max(metrics.widthPixels, metrics.heightPixels);
1363         // create a paragraph high enough to take up the entire screen
1364         String p =
1365                 "<p style=\"height:"
1366                         + dimension
1367                         + "px;\">"
1368                         + "Find all instances of find on the page and highlight them.</p>";
1369 
1370         mOnUiThread.loadDataAndWaitForCompletion(
1371                 "<html><body>" + p + "</body></html>", "text/html", null);
1372 
1373         WaitForFindResultsListener l = new WaitForFindResultsListener(mWebView, "find", 2, true);
1374         mOnUiThread.setFindListener(l);
1375         mOnUiThread.findAll("find");
1376         assertEquals(2, (int) WebkitUtils.waitForFuture(l.future()));
1377     }
1378 
1379     @Test
testFindNext()1380     public void testFindNext() throws Throwable {
1381         // Reset the scaling so that finding the next "all" text will require scrolling.
1382         mOnUiThread.setInitialScale(100);
1383 
1384         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
1385         int dimension = Math.max(metrics.widthPixels, metrics.heightPixels);
1386         // create a paragraph high enough to take up the entire screen
1387         String p =
1388                 "<p style=\"height:"
1389                         + dimension
1390                         + "px;\">"
1391                         + "Find all instances of a word on the page and highlight them.</p>";
1392 
1393         mOnUiThread.loadDataAndWaitForCompletion(
1394                 "<html><body>" + p + p + "</body></html>", "text/html", null);
1395         WaitForFindResultsListener l = new WaitForFindResultsListener(mWebView, "all", 2, true);
1396         mOnUiThread.setFindListener(l);
1397 
1398         // highlight all the strings found and wait for all the matches to be found
1399         mOnUiThread.findAll("all");
1400         WebkitUtils.waitForFuture(l.future());
1401         mOnUiThread.setFindListener(null);
1402 
1403         int previousScrollY = mOnUiThread.getScrollY();
1404 
1405         // Focus "all" in the second page and assert that the view scrolls.
1406         mOnUiThread.findNext(true);
1407         waitForScrollingComplete(previousScrollY);
1408         assertThat(mOnUiThread.getScrollY(), greaterThan(previousScrollY));
1409         previousScrollY = mOnUiThread.getScrollY();
1410 
1411         // Focus "all" in the first page and assert that the view scrolls.
1412         mOnUiThread.findNext(true);
1413         waitForScrollingComplete(previousScrollY);
1414         assertThat(mOnUiThread.getScrollY(), lessThan(previousScrollY));
1415         previousScrollY = mOnUiThread.getScrollY();
1416 
1417         // Focus "all" in the second page and assert that the view scrolls.
1418         mOnUiThread.findNext(false);
1419         waitForScrollingComplete(previousScrollY);
1420         assertThat(mOnUiThread.getScrollY(), greaterThan(previousScrollY));
1421         previousScrollY = mOnUiThread.getScrollY();
1422 
1423         // Focus "all" in the first page and assert that the view scrolls.
1424         mOnUiThread.findNext(false);
1425         waitForScrollingComplete(previousScrollY);
1426         assertThat(mOnUiThread.getScrollY(), lessThan(previousScrollY));
1427         previousScrollY = mOnUiThread.getScrollY();
1428 
1429         // clear the result
1430         mOnUiThread.clearMatches();
1431         getTestEnvironment().waitForIdleSync();
1432 
1433         // can not scroll any more
1434         mOnUiThread.findNext(false);
1435         waitForScrollingComplete(previousScrollY);
1436         assertEquals(mOnUiThread.getScrollY(), previousScrollY);
1437 
1438         mOnUiThread.findNext(true);
1439         waitForScrollingComplete(previousScrollY);
1440         assertEquals(mOnUiThread.getScrollY(), previousScrollY);
1441     }
1442 
1443     @Test
testDocumentHasImages()1444     public void testDocumentHasImages() throws Exception, Throwable {
1445         final class DocumentHasImageCheckHandler extends Handler {
1446             private SettableFuture<Integer> mFuture;
1447 
1448             public DocumentHasImageCheckHandler(Looper looper) {
1449                 super(looper);
1450                 mFuture = SettableFuture.create();
1451             }
1452 
1453             @Override
1454             public void handleMessage(Message msg) {
1455                 mFuture.set(msg.arg1);
1456             }
1457 
1458             public Future<Integer> future() {
1459                 return mFuture;
1460             }
1461         }
1462 
1463         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1464         final String imgUrl = mWebServer.getAssetUrl(TestHtmlConstants.SMALL_IMG_URL);
1465 
1466         // Create a handler on the UI thread.
1467         final DocumentHasImageCheckHandler handler =
1468                 new DocumentHasImageCheckHandler(mWebView.getHandler().getLooper());
1469 
1470         WebkitUtils.onMainThreadSync(
1471                 () -> {
1472                     mOnUiThread.loadDataAndWaitForCompletion(
1473                             "<html><body><img src=\"" + imgUrl + "\"/></body></html>",
1474                             "text/html",
1475                             null);
1476                     Message response = new Message();
1477                     response.setTarget(handler);
1478                     assertFalse(handler.future().isDone());
1479                     mWebView.documentHasImages(response);
1480                 });
1481         assertEquals(1, (int) WebkitUtils.waitForFuture(handler.future()));
1482     }
1483 
waitForFlingDone(WebViewOnUiThread webview)1484     private static void waitForFlingDone(WebViewOnUiThread webview) {
1485         class ScrollDiffPollingCheck extends PollingCheck {
1486             private static final long TIME_SLICE = 50;
1487             WebViewOnUiThread mWebView;
1488             private int mScrollX;
1489             private int mScrollY;
1490 
1491             ScrollDiffPollingCheck(WebViewOnUiThread webview) {
1492                 mWebView = webview;
1493                 mScrollX = mWebView.getScrollX();
1494                 mScrollY = mWebView.getScrollY();
1495             }
1496 
1497             @Override
1498             protected boolean check() {
1499                 try {
1500                     Thread.sleep(TIME_SLICE);
1501                 } catch (InterruptedException e) {
1502                     // Intentionally ignored.
1503                 }
1504                 int newScrollX = mWebView.getScrollX();
1505                 int newScrollY = mWebView.getScrollY();
1506                 boolean flingDone = newScrollX == mScrollX && newScrollY == mScrollY;
1507                 mScrollX = newScrollX;
1508                 mScrollY = newScrollY;
1509                 return flingDone;
1510             }
1511         }
1512         new ScrollDiffPollingCheck(webview).run();
1513     }
1514 
1515     @Test
testPageScroll()1516     public void testPageScroll() throws Throwable {
1517         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
1518         int dimension = 2 * Math.max(metrics.widthPixels, metrics.heightPixels);
1519         String p =
1520                 "<p style=\"height:"
1521                         + dimension
1522                         + "px;\">"
1523                         + "Scroll by half the size of the page.</p>";
1524         mOnUiThread.loadDataAndWaitForCompletion(
1525                 "<html><body>" + p + p + "</body></html>", "text/html", null);
1526 
1527         // Wait for UI thread to settle and receive page dimentions from renderer
1528         // such that we can invoke page down.
1529         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1530             @Override
1531             protected boolean check() {
1532                 return mOnUiThread.pageDown(false);
1533             }
1534         }.run();
1535 
1536         do {
1537             waitForFlingDone(mOnUiThread);
1538         } while (mOnUiThread.pageDown(false));
1539 
1540         waitForFlingDone(mOnUiThread);
1541         final int bottomScrollY = mOnUiThread.getScrollY();
1542 
1543         assertTrue(mOnUiThread.pageUp(false));
1544 
1545         do {
1546             waitForFlingDone(mOnUiThread);
1547         } while (mOnUiThread.pageUp(false));
1548 
1549         waitForFlingDone(mOnUiThread);
1550         final int topScrollY = mOnUiThread.getScrollY();
1551 
1552         // jump to the bottom
1553         assertTrue(mOnUiThread.pageDown(true));
1554         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1555             @Override
1556             protected boolean check() {
1557                 return bottomScrollY == mOnUiThread.getScrollY();
1558             }
1559         }.run();
1560 
1561         // jump to the top
1562         assertTrue(mOnUiThread.pageUp(true));
1563         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1564             @Override
1565             protected boolean check() {
1566                 return topScrollY == mOnUiThread.getScrollY();
1567             }
1568         }.run();
1569     }
1570 
1571     @Test
testGetContentHeight()1572     public void testGetContentHeight() throws Throwable {
1573         mOnUiThread.loadDataAndWaitForCompletion("<html><body></body></html>", "text/html", null);
1574         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1575             @Override
1576             protected boolean check() {
1577                 return mOnUiThread.getScale() != 0
1578                         && mOnUiThread.getContentHeight() != 0
1579                         && mOnUiThread.getHeight() != 0;
1580             }
1581         }.run();
1582 
1583         final int tolerance = 2;
1584         // getHeight() returns physical pixels and it is from web contents' size, getContentHeight()
1585         // returns CSS pixels and it is from compositor. In order to compare these two values, we
1586         // need to scale getContentHeight() by the device scale factor. This also amplifies any
1587         // rounding errors. Internally, getHeight() could also have rounding error first and then
1588         // times device scale factor, so we are comparing two rounded numbers below.
1589         // We allow 2 * getScale() as the delta, because getHeight() and getContentHeight() may
1590         // use different rounding algorithms and the results are from different computation
1591         // sequences. The extreme case is that in CSS pixel we have 2 as differences (0.9999 rounded
1592         // down and 1.0001 rounded up), therefore we ended with 2 * getScale().
1593         assertEquals(
1594                 mOnUiThread.getHeight(),
1595                 mOnUiThread.getContentHeight() * mOnUiThread.getScale(),
1596                 tolerance * Math.max(mOnUiThread.getScale(), 1.0f));
1597 
1598         // Make pageHeight bigger than the larger dimension of the device, so the page is taller
1599         // than viewport. Because when layout_height set to match_parent, getContentHeight() will
1600         // give maximum value between the actual web content height and the viewport height. When
1601         // viewport height is bigger, |extraSpace| below is not the extra space on the web page.
1602         // Note that we are passing physical pixels rather than CSS pixels here, when screen density
1603         // scale is lower than 1.0f, we need to scale it up.
1604         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
1605         final float scaleFactor = Math.max(1.0f, 1.0f / mOnUiThread.getScale());
1606         final int pageHeight =
1607                 (int)
1608                         (Math.ceil(
1609                                 Math.max(metrics.widthPixels, metrics.heightPixels) * scaleFactor));
1610 
1611         // set the margin to 0
1612         final String p =
1613                 "<p style=\"height:"
1614                         + pageHeight
1615                         + "px;margin:0px auto;\">Get the height of HTML content.</p>";
1616         mOnUiThread.loadDataAndWaitForCompletion(
1617                 "<html><body>" + p + "</body></html>", "text/html", null);
1618         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1619             @Override
1620             protected boolean check() {
1621                 return mOnUiThread.getContentHeight() > pageHeight;
1622             }
1623         }.run();
1624 
1625         final int extraSpace = mOnUiThread.getContentHeight() - pageHeight;
1626         mOnUiThread.loadDataAndWaitForCompletion(
1627                 "<html><body>" + p + p + "</body></html>", "text/html", null);
1628         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1629             @Override
1630             protected boolean check() {
1631                 // |pageHeight| is accurate, |extraSpace| = getContentheight() - |pageHeight|, so it
1632                 // might have rounding error +-1, also getContentHeight() might have rounding error
1633                 // +-1, so we allow error 2. Note that |pageHeight|, |extraSpace| and
1634                 // getContentHeight() are all CSS pixels.
1635                 final int expectedContentHeight = pageHeight + pageHeight + extraSpace;
1636                 return Math.abs(expectedContentHeight - mOnUiThread.getContentHeight())
1637                         <= tolerance;
1638             }
1639         }.run();
1640     }
1641 
1642     @Test
testPlatformNotifications()1643     public void testPlatformNotifications() {
1644         WebkitUtils.onMainThreadSync(
1645                 () -> {
1646                     WebView.enablePlatformNotifications();
1647                     WebView.disablePlatformNotifications();
1648                 });
1649     }
1650 
1651     @Test
testAccessPluginList()1652     public void testAccessPluginList() {
1653         WebkitUtils.onMainThreadSync(
1654                 () -> {
1655                     assertNotNull(WebView.getPluginList());
1656 
1657                     // can not find a way to install plugins
1658                     mWebView.refreshPlugins(false);
1659                 });
1660     }
1661 
1662     @Test
testDestroy()1663     public void testDestroy() {
1664         WebkitUtils.onMainThreadSync(
1665                 () -> {
1666                     // Create a new WebView, since we cannot call destroy() on a view in the
1667                     // hierarchy
1668                     WebView localWebView = new WebView(mContext);
1669                     localWebView.destroy();
1670                 });
1671     }
1672 
1673     @Test
testFlingScroll()1674     public void testFlingScroll() throws Throwable {
1675         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
1676         final int dimension = 10 * Math.max(metrics.widthPixels, metrics.heightPixels);
1677         String p =
1678                 "<p style=\"height:"
1679                         + dimension
1680                         + "px;"
1681                         + "width:"
1682                         + dimension
1683                         + "px\">Test fling scroll.</p>";
1684         mOnUiThread.loadDataAndWaitForCompletion(
1685                 "<html><body>" + p + "</body></html>", "text/html", null);
1686         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1687             @Override
1688             protected boolean check() {
1689                 return mOnUiThread.getContentHeight() >= dimension;
1690             }
1691         }.run();
1692 
1693         getTestEnvironment().waitForIdleSync();
1694 
1695         final int previousScrollX = mOnUiThread.getScrollX();
1696         final int previousScrollY = mOnUiThread.getScrollY();
1697 
1698         mOnUiThread.flingScroll(10000, 10000);
1699 
1700         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1701             @Override
1702             protected boolean check() {
1703                 return mOnUiThread.getScrollX() > previousScrollX
1704                         && mOnUiThread.getScrollY() > previousScrollY;
1705             }
1706         }.run();
1707     }
1708 
1709     @Test
testRequestFocusNodeHref()1710     public void testRequestFocusNodeHref() throws Throwable {
1711         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1712 
1713         final String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
1714         final String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
1715         final String links =
1716                 "<DL><p><DT><A HREF=\""
1717                         + url1
1718                         + "\">HTML_URL1</A><DT><A HREF=\""
1719                         + url2
1720                         + "\">HTML_URL2</A></DL><p>";
1721         mOnUiThread.loadDataAndWaitForCompletion(
1722                 "<html><body>" + links + "</body></html>", "text/html", null);
1723         getTestEnvironment().waitForIdleSync();
1724 
1725         final HrefCheckHandler handler = new HrefCheckHandler(mWebView.getHandler().getLooper());
1726         final Message hrefMsg = new Message();
1727         hrefMsg.setTarget(handler);
1728 
1729         // focus on first link
1730         handler.reset();
1731         getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
1732         mOnUiThread.requestFocusNodeHref(hrefMsg);
1733         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1734             @Override
1735             protected boolean check() {
1736                 boolean done = false;
1737                 if (handler.hasCalledHandleMessage()) {
1738                     if (handler.mResultUrl != null) {
1739                         done = true;
1740                     } else {
1741                         handler.reset();
1742                         Message newMsg = new Message();
1743                         newMsg.setTarget(handler);
1744                         mOnUiThread.requestFocusNodeHref(newMsg);
1745                     }
1746                 }
1747                 return done;
1748             }
1749         }.run();
1750         assertEquals(url1, handler.getResultUrl());
1751 
1752         // focus on second link
1753         handler.reset();
1754         final Message hrefMsg2 = new Message();
1755         hrefMsg2.setTarget(handler);
1756         getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
1757         mOnUiThread.requestFocusNodeHref(hrefMsg2);
1758         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1759             @Override
1760             protected boolean check() {
1761                 boolean done = false;
1762                 if (handler.hasCalledHandleMessage()) {
1763                     if (handler.mResultUrl != null && handler.mResultUrl.equals(url2)) {
1764                         done = true;
1765                     } else {
1766                         handler.reset();
1767                         Message newMsg = new Message();
1768                         newMsg.setTarget(handler);
1769                         mOnUiThread.requestFocusNodeHref(newMsg);
1770                     }
1771                 }
1772                 return done;
1773             }
1774         }.run();
1775         assertEquals(url2, handler.getResultUrl());
1776 
1777         mOnUiThread.requestFocusNodeHref(null);
1778     }
1779 
1780     @Test
testRequestImageRef()1781     public void testRequestImageRef() throws Exception, Throwable {
1782         final class ImageLoaded {
1783             public SettableFuture<Void> mImageLoaded = SettableFuture.create();
1784 
1785             @JavascriptInterface
1786             public void loaded() {
1787                 mImageLoaded.set(null);
1788             }
1789 
1790             public Future<Void> future() {
1791                 return mImageLoaded;
1792             }
1793         }
1794         final ImageLoaded imageLoaded = new ImageLoaded();
1795         mOnUiThread.getSettings().setJavaScriptEnabled(true);
1796         mOnUiThread.addJavascriptInterface(imageLoaded, "imageLoaded");
1797 
1798         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1799         final String imgUrl = mWebServer.getAssetUrl(TestHtmlConstants.LARGE_IMG_URL);
1800         mOnUiThread.loadDataAndWaitForCompletion(
1801                 "<html><head><title>Title</title><style type='text/css'>"
1802                         + "%23imgElement { -webkit-transform: translate3d(0,0,1); }"
1803                         + "%23imgElement.finish { -webkit-transform: translate3d(0,0,0);"
1804                         + " -webkit-transition-duration: 1ms; }</style>"
1805                         + "<script type='text/javascript'>function imgLoad() {"
1806                         + "imgElement = document.getElementById('imgElement');"
1807                         + "imgElement.addEventListener('webkitTransitionEnd',"
1808                         + "function(e) { imageLoaded.loaded(); });"
1809                         + "imgElement.className = 'finish';}</script>"
1810                         + "</head><body><img id='imgElement' src='"
1811                         + imgUrl
1812                         + "' width='100%' height='100%' onLoad='imgLoad()'/>"
1813                         + "</body></html>",
1814                 "text/html",
1815                 null);
1816         WebkitUtils.waitForFuture(imageLoaded.future());
1817         getTestEnvironment().waitForIdleSync();
1818 
1819         final HrefCheckHandler handler = new HrefCheckHandler(mWebView.getHandler().getLooper());
1820         final Message msg = new Message();
1821         msg.setTarget(handler);
1822 
1823         // touch the image
1824         handler.reset();
1825         int[] location = mOnUiThread.getLocationOnScreen();
1826         int middleX = location[0] + mOnUiThread.getWebView().getWidth() / 2;
1827         int middleY = location[1] + mOnUiThread.getWebView().getHeight() / 2;
1828         getTestEnvironment().sendTapSync(middleX, middleY);
1829 
1830         getTestEnvironment().waitForIdleSync();
1831         mOnUiThread.requestImageRef(msg);
1832         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1833             @Override
1834             protected boolean check() {
1835                 boolean done = false;
1836                 if (handler.hasCalledHandleMessage()) {
1837                     if (handler.mResultUrl != null) {
1838                         done = true;
1839                     } else {
1840                         handler.reset();
1841                         Message newMsg = new Message();
1842                         newMsg.setTarget(handler);
1843                         mOnUiThread.requestImageRef(newMsg);
1844                     }
1845                 }
1846                 return done;
1847             }
1848         }.run();
1849         assertEquals(imgUrl, handler.mResultUrl);
1850     }
1851 
1852     @Test
testDebugDump()1853     public void testDebugDump() {
1854         WebkitUtils.onMainThreadSync(
1855                 () -> {
1856                     mWebView.debugDump();
1857                 });
1858     }
1859 
1860     @Test
testGetHitTestResult()1861     public void testGetHitTestResult() throws Throwable {
1862         final String anchor =
1863                 "<p><a href=\"" + TestHtmlConstants.EXT_WEB_URL1 + "\">normal anchor</a></p>";
1864         final String blankAnchor = "<p><a href=\"\">blank anchor</a></p>";
1865         final String form =
1866                 "<p><form><input type=\"text\" name=\"Test\"><br>"
1867                         + "<input type=\"submit\" value=\"Submit\"></form></p>";
1868         String phoneNo = "3106984000";
1869         final String tel = "<p><a href=\"tel:" + phoneNo + "\">Phone</a></p>";
1870         String email = "[email protected]";
1871         final String mailto = "<p><a href=\"mailto:" + email + "\">Email</a></p>";
1872         String location = "shanghai";
1873         final String geo = "<p><a href=\"geo:0,0?q=" + location + "\">Location</a></p>";
1874 
1875         mOnUiThread.loadDataWithBaseURLAndWaitForCompletion(
1876                 "fake://home",
1877                 "<html><body>"
1878                         + anchor
1879                         + blankAnchor
1880                         + form
1881                         + tel
1882                         + mailto
1883                         + geo
1884                         + "</body></html>",
1885                 "text/html",
1886                 "UTF-8",
1887                 null);
1888         getTestEnvironment().waitForIdleSync();
1889 
1890         // anchor
1891         moveFocusDown();
1892         HitTestResult hitTestResult = mOnUiThread.getHitTestResult();
1893         assertEquals(HitTestResult.SRC_ANCHOR_TYPE, hitTestResult.getType());
1894         assertEquals(TestHtmlConstants.EXT_WEB_URL1, hitTestResult.getExtra());
1895 
1896         // blank anchor
1897         moveFocusDown();
1898         hitTestResult = mOnUiThread.getHitTestResult();
1899         assertEquals(HitTestResult.SRC_ANCHOR_TYPE, hitTestResult.getType());
1900         assertEquals("fake://home", hitTestResult.getExtra());
1901 
1902         // text field
1903         moveFocusDown();
1904         hitTestResult = mOnUiThread.getHitTestResult();
1905         assertEquals(HitTestResult.EDIT_TEXT_TYPE, hitTestResult.getType());
1906         assertNull(hitTestResult.getExtra());
1907 
1908         // submit button
1909         moveFocusDown();
1910         hitTestResult = mOnUiThread.getHitTestResult();
1911         assertEquals(HitTestResult.UNKNOWN_TYPE, hitTestResult.getType());
1912         assertNull(hitTestResult.getExtra());
1913 
1914         // phone number
1915         moveFocusDown();
1916         hitTestResult = mOnUiThread.getHitTestResult();
1917         assertEquals(HitTestResult.PHONE_TYPE, hitTestResult.getType());
1918         assertEquals(phoneNo, hitTestResult.getExtra());
1919 
1920         // email
1921         moveFocusDown();
1922         hitTestResult = mOnUiThread.getHitTestResult();
1923         assertEquals(HitTestResult.EMAIL_TYPE, hitTestResult.getType());
1924         assertEquals(email, hitTestResult.getExtra());
1925 
1926         // geo address
1927         moveFocusDown();
1928         hitTestResult = mOnUiThread.getHitTestResult();
1929         assertEquals(HitTestResult.GEO_TYPE, hitTestResult.getType());
1930         assertEquals(location, hitTestResult.getExtra());
1931     }
1932 
1933     @Test
testSetInitialScale()1934     public void testSetInitialScale() throws Throwable {
1935         final String p = "<p style=\"height:1000px;width:1000px\">Test setInitialScale.</p>";
1936         final float defaultScale = mContext.getResources().getDisplayMetrics().density;
1937 
1938         mOnUiThread.loadDataAndWaitForCompletion(
1939                 "<html><body>" + p + "</body></html>", "text/html", null);
1940 
1941         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1942             @Override
1943             protected boolean check() {
1944                 return Math.abs(defaultScale - mOnUiThread.getScale()) < .01f;
1945             }
1946         }.run();
1947 
1948         mOnUiThread.setInitialScale(0);
1949         // modify content to fool WebKit into re-loading
1950         mOnUiThread.loadDataAndWaitForCompletion(
1951                 "<html><body>" + p + "2" + "</body></html>", "text/html", null);
1952 
1953         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1954             @Override
1955             protected boolean check() {
1956                 return Math.abs(defaultScale - mOnUiThread.getScale()) < .01f;
1957             }
1958         }.run();
1959 
1960         mOnUiThread.setInitialScale(50);
1961         mOnUiThread.loadDataAndWaitForCompletion(
1962                 "<html><body>" + p + "3" + "</body></html>", "text/html", null);
1963 
1964         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1965             @Override
1966             protected boolean check() {
1967                 return Math.abs(0.5 - mOnUiThread.getScale()) < .01f;
1968             }
1969         }.run();
1970 
1971         mOnUiThread.setInitialScale(0);
1972         mOnUiThread.loadDataAndWaitForCompletion(
1973                 "<html><body>" + p + "4" + "</body></html>", "text/html", null);
1974 
1975         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
1976             @Override
1977             protected boolean check() {
1978                 return Math.abs(defaultScale - mOnUiThread.getScale()) < .01f;
1979             }
1980         }.run();
1981     }
1982 
1983     @Test
testClearHistory()1984     public void testClearHistory() throws Exception {
1985         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
1986 
1987         String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
1988         String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
1989         String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
1990 
1991         mOnUiThread.loadUrlAndWaitForCompletion(url1);
1992         pollingCheckWebBackForwardList(url1, 0, 1);
1993 
1994         mOnUiThread.loadUrlAndWaitForCompletion(url2);
1995         pollingCheckWebBackForwardList(url2, 1, 2);
1996 
1997         mOnUiThread.loadUrlAndWaitForCompletion(url3);
1998         pollingCheckWebBackForwardList(url3, 2, 3);
1999 
2000         mOnUiThread.clearHistory();
2001 
2002         // only current URL is left after clearing
2003         pollingCheckWebBackForwardList(url3, 0, 1);
2004     }
2005 
2006     @Test
testSaveAndRestoreState()2007     public void testSaveAndRestoreState() throws Throwable {
2008         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
2009 
2010         assertNull(
2011                 "Should return null when there's nothing to save",
2012                 mOnUiThread.saveState(new Bundle()));
2013 
2014         String url1 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL1);
2015         String url2 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL2);
2016         String url3 = mWebServer.getAssetUrl(TestHtmlConstants.HTML_URL3);
2017 
2018         // make a history list
2019         mOnUiThread.loadUrlAndWaitForCompletion(url1);
2020         pollingCheckWebBackForwardList(url1, 0, 1);
2021         mOnUiThread.loadUrlAndWaitForCompletion(url2);
2022         pollingCheckWebBackForwardList(url2, 1, 2);
2023         mOnUiThread.loadUrlAndWaitForCompletion(url3);
2024         pollingCheckWebBackForwardList(url3, 2, 3);
2025 
2026         // save the list
2027         Bundle bundle = new Bundle();
2028         WebBackForwardList saveList = mOnUiThread.saveState(bundle);
2029         assertNotNull(saveList);
2030         assertEquals(3, saveList.getSize());
2031         assertEquals(2, saveList.getCurrentIndex());
2032         assertEquals(url1, saveList.getItemAtIndex(0).getUrl());
2033         assertEquals(url2, saveList.getItemAtIndex(1).getUrl());
2034         assertEquals(url3, saveList.getItemAtIndex(2).getUrl());
2035 
2036         WebViewOnUiThread newOnUiThread = new WebViewOnUiThread(WebkitUtils.onMainThreadSync(
2037                 () -> {
2038                     // change the content to a new "blank" web view without history
2039                     return new WebView(mContext);
2040                 }));
2041         try {
2042             WebBackForwardList copyListBeforeRestore = newOnUiThread.copyBackForwardList();
2043             assertNotNull(copyListBeforeRestore);
2044             assertEquals(0, copyListBeforeRestore.getSize());
2045 
2046             // restore the list
2047             final WebBackForwardList restoreList = newOnUiThread.restoreState(bundle);
2048             assertNotNull(restoreList);
2049             assertEquals(3, restoreList.getSize());
2050             assertEquals(2, saveList.getCurrentIndex());
2051 
2052             // wait for the list items to get inflated
2053             new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2054                 @Override
2055                 protected boolean check() {
2056                     return restoreList.getItemAtIndex(0).getUrl() != null
2057                             && restoreList.getItemAtIndex(1).getUrl() != null
2058                             && restoreList.getItemAtIndex(2).getUrl() != null;
2059                 }
2060             }.run();
2061             assertEquals(url1, restoreList.getItemAtIndex(0).getUrl());
2062             assertEquals(url2, restoreList.getItemAtIndex(1).getUrl());
2063             assertEquals(url3, restoreList.getItemAtIndex(2).getUrl());
2064 
2065             WebBackForwardList copyListAfterRestore = newOnUiThread.copyBackForwardList();
2066             assertNotNull(copyListAfterRestore);
2067             assertEquals(3, copyListAfterRestore.getSize());
2068             assertEquals(2, copyListAfterRestore.getCurrentIndex());
2069             assertEquals(url1, copyListAfterRestore.getItemAtIndex(0).getUrl());
2070             assertEquals(url2, copyListAfterRestore.getItemAtIndex(1).getUrl());
2071             assertEquals(url3, copyListAfterRestore.getItemAtIndex(2).getUrl());
2072         } finally {
2073             newOnUiThread.cleanUp();
2074         }
2075     }
2076 
2077     @Test
testRequestChildRectangleOnScreen()2078     public void testRequestChildRectangleOnScreen() throws Throwable {
2079         // It is needed to make test pass on some devices.
2080         mOnUiThread.setLayoutToMatchParent();
2081 
2082         DisplayMetrics metrics = mOnUiThread.getDisplayMetrics();
2083         final int dimension = 2 * Math.max(metrics.widthPixels, metrics.heightPixels);
2084         String p = "<p style=\"height:" + dimension + "px;width:" + dimension + "px\">&nbsp;</p>";
2085         mOnUiThread.loadDataAndWaitForCompletion(
2086                 "<html><body>" + p + "</body></html>", "text/html", null);
2087         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2088             @Override
2089             protected boolean check() {
2090                 return mOnUiThread.getContentHeight() >= dimension;
2091             }
2092         }.run();
2093 
2094         int origX = mOnUiThread.getScrollX();
2095         int origY = mOnUiThread.getScrollY();
2096         int half = dimension / 2;
2097         Rect rect = new Rect(half, half, half + 1, half + 1);
2098         assertTrue(mOnUiThread.requestChildRectangleOnScreen(mWebView, rect, true));
2099         // In a few cases the values returned by getScrollX/getScrollY don't update immediately
2100         // even though the scroll was in fact issued, so we poll for it.
2101         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2102             @Override
2103             protected boolean check() {
2104                 return mOnUiThread.getScrollX() > origX && mOnUiThread.getScrollY() > origY;
2105             }
2106         }.run();
2107     }
2108 
2109     @Test
testSetDownloadListener()2110     public void testSetDownloadListener() throws Throwable {
2111         final SettableFuture<Void> downloadStartFuture = SettableFuture.create();
2112         final class MyDownloadListener implements DownloadListener {
2113             public String url;
2114             public String mimeType;
2115             public long contentLength;
2116             public String contentDisposition;
2117 
2118             @Override
2119             public void onDownloadStart(
2120                     String url,
2121                     String userAgent,
2122                     String contentDisposition,
2123                     String mimetype,
2124                     long contentLength) {
2125                 this.url = url;
2126                 this.mimeType = mimetype;
2127                 this.contentLength = contentLength;
2128                 this.contentDisposition = contentDisposition;
2129                 downloadStartFuture.set(null);
2130             }
2131         }
2132 
2133         final String mimeType = "application/octet-stream";
2134         final int length = 100;
2135         final MyDownloadListener listener = new MyDownloadListener();
2136 
2137         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
2138         final String url = mWebServer.getBinaryUrl(mimeType, length);
2139 
2140         // By default, WebView sends an intent to ask the system to
2141         // handle loading a new URL. We set WebViewClient as
2142         // WebViewClient.shouldOverrideUrlLoading() returns false, so
2143         // the WebView will load the new URL.
2144         mOnUiThread.setDownloadListener(listener);
2145         mOnUiThread.getSettings().setJavaScriptEnabled(true);
2146         mOnUiThread.loadDataAndWaitForCompletion(
2147                 "<html><body onload=\"window.location = \'" + url + "\'\"></body></html>",
2148                 "text/html",
2149                 null);
2150         // Wait for layout to complete before setting focus.
2151         getTestEnvironment().waitForIdleSync();
2152 
2153         WebkitUtils.waitForFuture(downloadStartFuture);
2154         assertEquals(url, listener.url);
2155         assertTrue(listener.contentDisposition.contains("test.bin"));
2156         assertEquals(length, listener.contentLength);
2157         assertEquals(mimeType, listener.mimeType);
2158     }
2159 
2160     @Test
testSetLayoutParams()2161     public void testSetLayoutParams() {
2162         WebkitUtils.onMainThreadSync(
2163                 () -> {
2164                     LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(600, 800);
2165                     mWebView.setLayoutParams(params);
2166                     assertSame(params, mWebView.getLayoutParams());
2167                 });
2168     }
2169 
2170     @Test
testSetMapTrackballToArrowKeys()2171     public void testSetMapTrackballToArrowKeys() {
2172         WebkitUtils.onMainThreadSync(
2173                 () -> {
2174                     mWebView.setMapTrackballToArrowKeys(true);
2175                 });
2176     }
2177 
2178     @Test
testSetNetworkAvailable()2179     public void testSetNetworkAvailable() throws Exception {
2180         WebSettings settings = mOnUiThread.getSettings();
2181         settings.setJavaScriptEnabled(true);
2182         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
2183 
2184         String url = mWebServer.getAssetUrl(TestHtmlConstants.NETWORK_STATE_URL);
2185         mOnUiThread.loadUrlAndWaitForCompletion(url);
2186         assertEquals("ONLINE", mOnUiThread.getTitle());
2187 
2188         mOnUiThread.setNetworkAvailable(false);
2189 
2190         // Wait for the DOM to receive notification of the network state change.
2191         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2192             @Override
2193             protected boolean check() {
2194                 return mOnUiThread.getTitle().equals("OFFLINE");
2195             }
2196         }.run();
2197 
2198         mOnUiThread.setNetworkAvailable(true);
2199 
2200         // Wait for the DOM to receive notification of the network state change.
2201         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2202             @Override
2203             protected boolean check() {
2204                 return mOnUiThread.getTitle().equals("ONLINE");
2205             }
2206         }.run();
2207     }
2208 
2209     @Test
testSetWebChromeClient()2210     public void testSetWebChromeClient() throws Throwable {
2211         final SettableFuture<Void> future = SettableFuture.create();
2212         mOnUiThread.setWebChromeClient(
2213                 new WaitForProgressClient(mOnUiThread) {
2214                     @Override
2215                     public void onProgressChanged(WebView view, int newProgress) {
2216                         super.onProgressChanged(view, newProgress);
2217                         future.set(null);
2218                     }
2219                 });
2220         getTestEnvironment().waitForIdleSync();
2221         assertFalse(future.isDone());
2222 
2223         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
2224         final String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
2225         mOnUiThread.loadUrlAndWaitForCompletion(url);
2226         getTestEnvironment().waitForIdleSync();
2227 
2228         WebkitUtils.waitForFuture(future);
2229     }
2230 
2231     @Test
testPauseResumeTimers()2232     public void testPauseResumeTimers() throws Throwable {
2233         class Monitor {
2234             private boolean mIsUpdated;
2235 
2236             @JavascriptInterface
2237             public synchronized void update() {
2238                 mIsUpdated = true;
2239                 notify();
2240             }
2241 
2242             public synchronized boolean waitForUpdate() {
2243                 while (!mIsUpdated) {
2244                     try {
2245                         // This is slightly flaky, as we can't guarantee that
2246                         // this is a sufficient time limit, but there's no way
2247                         // around this.
2248                         wait(1000);
2249                         if (!mIsUpdated) {
2250                             return false;
2251                         }
2252                     } catch (InterruptedException e) {
2253                     }
2254                 }
2255                 mIsUpdated = false;
2256                 return true;
2257             }
2258         }
2259 
2260         final Monitor monitor = new Monitor();
2261         final String updateMonitorHtml =
2262                 "<html>" + "<body onload=\"monitor.update();\"></body></html>";
2263 
2264         // Test that JavaScript is executed even with timers paused.
2265         WebkitUtils.onMainThreadSync(
2266                 () -> {
2267                     mWebView.getSettings().setJavaScriptEnabled(true);
2268                     mWebView.addJavascriptInterface(monitor, "monitor");
2269                     mWebView.pauseTimers();
2270                     mOnUiThread.loadDataAndWaitForCompletion(updateMonitorHtml, "text/html", null);
2271                 });
2272         assertTrue(monitor.waitForUpdate());
2273 
2274         // Start a timer and test that it does not fire.
2275         mOnUiThread.loadDataAndWaitForCompletion(
2276                 "<html><body onload='setTimeout(function(){monitor.update();},100)'>"
2277                         + "</body></html>",
2278                 "text/html",
2279                 null);
2280         assertFalse(monitor.waitForUpdate());
2281 
2282         // Resume timers and test that the timer fires.
2283         mOnUiThread.resumeTimers();
2284         assertTrue(monitor.waitForUpdate());
2285     }
2286 
2287     // verify query parameters can be passed correctly to android asset files
2288     @Test
testAndroidAssetQueryParam()2289     public void testAndroidAssetQueryParam() {
2290         WebSettings settings = mOnUiThread.getSettings();
2291         settings.setJavaScriptEnabled(true);
2292         // test passing a parameter
2293         String fileUrl =
2294                 TestHtmlConstants.getFileUrl(TestHtmlConstants.PARAM_ASSET_URL + "?val=SUCCESS");
2295         mOnUiThread.loadUrlAndWaitForCompletion(fileUrl);
2296         assertEquals("SUCCESS", mOnUiThread.getTitle());
2297     }
2298 
2299     // verify anchors work correctly for android asset files
2300     @Test
testAndroidAssetAnchor()2301     public void testAndroidAssetAnchor() {
2302         WebSettings settings = mOnUiThread.getSettings();
2303         settings.setJavaScriptEnabled(true);
2304         // test using an anchor
2305         String fileUrl =
2306                 TestHtmlConstants.getFileUrl(TestHtmlConstants.ANCHOR_ASSET_URL + "#anchor");
2307         mOnUiThread.loadUrlAndWaitForCompletion(fileUrl);
2308         assertEquals("anchor", mOnUiThread.getTitle());
2309     }
2310 
2311     @Test
testEvaluateJavascript()2312     public void testEvaluateJavascript() {
2313         mOnUiThread.getSettings().setJavaScriptEnabled(true);
2314         mOnUiThread.loadUrlAndWaitForCompletion("about:blank");
2315 
2316         assertEquals("2", mOnUiThread.evaluateJavascriptSync("1+1"));
2317 
2318         assertEquals("9", mOnUiThread.evaluateJavascriptSync("1+1; 4+5"));
2319 
2320         final String EXPECTED_TITLE = "test";
2321         mOnUiThread.evaluateJavascript("document.title='" + EXPECTED_TITLE + "';", null);
2322         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2323             @Override
2324             protected boolean check() {
2325                 return mOnUiThread.getTitle().equals(EXPECTED_TITLE);
2326             }
2327         }.run();
2328     }
2329 
2330     // Verify Print feature can create a PDF file with a correct preamble.
2331     @Test
testPrinting()2332     public void testPrinting() throws Throwable {
2333         mOnUiThread.loadDataAndWaitForCompletion(
2334                 "<html><head></head>" + "<body>foo</body></html>", "text/html", null);
2335         final PrintDocumentAdapter adapter = mOnUiThread.createPrintDocumentAdapter();
2336         printDocumentStart(adapter);
2337         PrintAttributes attributes =
2338                 new PrintAttributes.Builder()
2339                         .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
2340                         .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
2341                         .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
2342                         .build();
2343         final File file = mContext.getFileStreamPath(PRINTER_TEST_FILE);
2344         final ParcelFileDescriptor descriptor =
2345                 ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode("w"));
2346         final SettableFuture<Void> result = SettableFuture.create();
2347         printDocumentLayout(
2348                 adapter,
2349                 null,
2350                 attributes,
2351                 new LayoutResultCallback() {
2352                     // Called on UI thread
2353                     @Override
2354                     public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
2355                         PageRange[] pageRanges = new PageRange[] {PageRange.ALL_PAGES};
2356                         savePrintedPage(adapter, descriptor, pageRanges, result);
2357                     }
2358                 });
2359         try {
2360             WebkitUtils.waitForFuture(result);
2361             assertThat(file.length(), greaterThan(0L));
2362             FileInputStream in = new FileInputStream(file);
2363             byte[] b = new byte[PDF_PREAMBLE.length()];
2364             in.read(b);
2365             String preamble = new String(b);
2366             assertEquals(PDF_PREAMBLE, preamble);
2367         } finally {
2368             // close the descriptor, if not closed already.
2369             descriptor.close();
2370             file.delete();
2371         }
2372     }
2373 
2374     // Verify Print feature can create a PDF file with correct number of pages.
2375     @Test
testPrintingPagesCount()2376     public void testPrintingPagesCount() throws Throwable {
2377         String content = "<html><head></head><body>";
2378         for (int i = 0; i < 500; ++i) {
2379             content += "<br />abcdefghijk<br />";
2380         }
2381         content += "</body></html>";
2382         mOnUiThread.loadDataAndWaitForCompletion(content, "text/html", null);
2383         final PrintDocumentAdapter adapter = mOnUiThread.createPrintDocumentAdapter();
2384         printDocumentStart(adapter);
2385         PrintAttributes attributes =
2386                 new PrintAttributes.Builder()
2387                         .setMediaSize(PrintAttributes.MediaSize.ISO_A4)
2388                         .setResolution(new PrintAttributes.Resolution("foo", "bar", 300, 300))
2389                         .setMinMargins(PrintAttributes.Margins.NO_MARGINS)
2390                         .build();
2391         final File file = mContext.getFileStreamPath(PRINTER_TEST_FILE);
2392         final ParcelFileDescriptor descriptor =
2393                 ParcelFileDescriptor.open(file, ParcelFileDescriptor.parseMode("w"));
2394         final SettableFuture<Void> result = SettableFuture.create();
2395         printDocumentLayout(
2396                 adapter,
2397                 null,
2398                 attributes,
2399                 new LayoutResultCallback() {
2400                     // Called on UI thread
2401                     @Override
2402                     public void onLayoutFinished(PrintDocumentInfo info, boolean changed) {
2403                         PageRange[] pageRanges =
2404                                 new PageRange[] {new PageRange(1, 1), new PageRange(4, 7)};
2405                         savePrintedPage(adapter, descriptor, pageRanges, result);
2406                     }
2407                 });
2408         try {
2409             WebkitUtils.waitForFuture(result);
2410             assertThat(file.length(), greaterThan(0L));
2411             PdfRenderer renderer =
2412                     new PdfRenderer(
2413                             ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
2414             assertEquals(5, renderer.getPageCount());
2415         } finally {
2416             descriptor.close();
2417             file.delete();
2418         }
2419     }
2420 
2421     /**
2422      * This should remain functionally equivalent to
2423      * androidx.webkit.WebViewCompatTest#testVisualStateCallbackCalled. Modifications to this test
2424      * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
2425      */
2426     @Test
testVisualStateCallbackCalled()2427     public void testVisualStateCallbackCalled() throws Exception {
2428         // Check that the visual state callback is called correctly.
2429         final long kRequest = 100;
2430 
2431         mOnUiThread.loadUrl("about:blank");
2432 
2433         final SettableFuture<Long> visualStateFuture = SettableFuture.create();
2434         mOnUiThread.postVisualStateCallback(
2435                 kRequest,
2436                 new VisualStateCallback() {
2437                     public void onComplete(long requestId) {
2438                         visualStateFuture.set(requestId);
2439                     }
2440                 });
2441 
2442         assertEquals(kRequest, (long) WebkitUtils.waitForFuture(visualStateFuture));
2443     }
2444 
setSafeBrowsingAllowlistSync(List<String> allowlist)2445     private static boolean setSafeBrowsingAllowlistSync(List<String> allowlist) {
2446         final SettableFuture<Boolean> safeBrowsingAllowlistFuture = SettableFuture.create();
2447         WebView.setSafeBrowsingWhitelist(
2448                 allowlist,
2449                 new ValueCallback<Boolean>() {
2450                     @Override
2451                     public void onReceiveValue(Boolean success) {
2452                         safeBrowsingAllowlistFuture.set(success);
2453                     }
2454                 });
2455         return WebkitUtils.waitForFuture(safeBrowsingAllowlistFuture);
2456     }
2457 
2458     /**
2459      * This should remain functionally equivalent to
2460      * androidx.webkit.WebViewCompatTest#testSetSafeBrowsingAllowlistWithMalformedList.
2461      * Modifications to this test should be reflected in that test as necessary. See
2462      * http://go/modifying-webview-cts.
2463      */
2464     @Test
testSetSafeBrowsingAllowlistWithMalformedList()2465     public void testSetSafeBrowsingAllowlistWithMalformedList() throws Exception {
2466         List allowlist = new ArrayList<String>();
2467         // Protocols are not supported in the allowlist
2468         allowlist.add("http://google.com");
2469         assertFalse("Malformed list entry should fail", setSafeBrowsingAllowlistSync(allowlist));
2470     }
2471 
2472     /**
2473      * This should remain functionally equivalent to
2474      * androidx.webkit.WebViewCompatTest#testSetSafeBrowsingAllowlistWithValidList. Modifications to
2475      * this test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
2476      */
2477     @Test
2478     @Ignore("b/368230178 - re-enable when WebView M131 is available in Android main branch")
testSetSafeBrowsingAllowlistWithValidList()2479     public void testSetSafeBrowsingAllowlistWithValidList() throws Exception {
2480         List allowlist = new ArrayList<String>();
2481         allowlist.add("safe-browsing");
2482         assertTrue("Valid allowlist should be successful", setSafeBrowsingAllowlistSync(allowlist));
2483 
2484         final SettableFuture<Void> pageFinishedFuture = SettableFuture.create();
2485         mOnUiThread.setWebViewClient(
2486                 new WebViewClient() {
2487                     @Override
2488                     public void onPageFinished(WebView view, String url) {
2489                         pageFinishedFuture.set(null);
2490                     }
2491 
2492                     @Override
2493                     public void onSafeBrowsingHit(
2494                             WebView view,
2495                             WebResourceRequest request,
2496                             int threatType,
2497                             SafeBrowsingResponse callback) {
2498                         pageFinishedFuture.setException(
2499                                 new IllegalStateException(
2500                                         "Should not invoke onSafeBrowsingHit for "
2501                                                 + request.getUrl()));
2502                     }
2503                 });
2504 
2505         mOnUiThread.loadUrl("chrome://safe-browsing/match?type=malware");
2506 
2507         // Wait until page load has completed
2508         WebkitUtils.waitForFuture(pageFinishedFuture);
2509     }
2510 
2511     /**
2512      * This should remain functionally equivalent to
2513      * androidx.webkit.WebViewCompatTest#testGetWebViewClient. Modifications to this test should be
2514      * reflected in that test as necessary. See http://go/modifying-webview-cts.
2515      */
2516     @Test
testGetWebViewClient()2517     public void testGetWebViewClient() throws Exception {
2518         WebkitUtils.onMainThreadSync(
2519                 () -> {
2520                     // getWebViewClient should return a default WebViewClient if it hasn't been set
2521                     // yet
2522                     WebView webView = new WebView(mContext);
2523                     WebViewClient client = webView.getWebViewClient();
2524                     assertNotNull(client);
2525                     assertTrue(client instanceof WebViewClient);
2526 
2527                     // getWebViewClient should return the client after it has been set
2528                     WebViewClient client2 = new WebViewClient();
2529                     assertNotSame(client, client2);
2530                     webView.setWebViewClient(client2);
2531                     assertSame(client2, webView.getWebViewClient());
2532                     webView.destroy();
2533                 });
2534     }
2535 
2536     /**
2537      * This should remain functionally equivalent to
2538      * androidx.webkit.WebViewCompatTest#testGetWebChromeClient. Modifications to this test should
2539      * be reflected in that test as necessary. See http://go/modifying-webview-cts.
2540      */
2541     @Test
testGetWebChromeClient()2542     public void testGetWebChromeClient() throws Exception {
2543         WebkitUtils.onMainThreadSync(
2544                 () -> {
2545                     // getWebChromeClient should return null if the client hasn't been set yet
2546                     WebView webView = new WebView(mContext);
2547                     WebChromeClient client = webView.getWebChromeClient();
2548                     assertNull(client);
2549 
2550                     // getWebChromeClient should return the client after it has been set
2551                     WebChromeClient client2 = new WebChromeClient();
2552                     assertNotSame(client, client2);
2553                     webView.setWebChromeClient(client2);
2554                     assertSame(client2, webView.getWebChromeClient());
2555                     webView.destroy();
2556                 });
2557     }
2558 
2559     @Test
testSetCustomTextClassifier()2560     public void testSetCustomTextClassifier() throws Exception {
2561         class CustomTextClassifier implements TextClassifier {
2562             @Override
2563             public TextSelection suggestSelection(
2564                     CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) {
2565                 return new TextSelection.Builder(0, 1).build();
2566             }
2567 
2568             @Override
2569             public TextClassification classifyText(
2570                     CharSequence text, int startIndex, int endIndex, LocaleList defaultLocales) {
2571                 return new TextClassification.Builder().build();
2572             }
2573         }
2574 
2575         WebkitUtils.onMainThreadSync(
2576                 () -> {
2577                     TextClassifier classifier = new CustomTextClassifier();
2578                     WebView webView = new WebView(mContext);
2579                     webView.setTextClassifier(classifier);
2580                     assertSame(webView.getTextClassifier(), classifier);
2581                     webView.destroy();
2582                 });
2583     }
2584 
2585     private static class MockContext extends ContextWrapper {
2586         private boolean mGetApplicationContextWasCalled;
2587 
MockContext(Context context)2588         public MockContext(Context context) {
2589             super(context);
2590         }
2591 
getApplicationContext()2592         public Context getApplicationContext() {
2593             mGetApplicationContextWasCalled = true;
2594             return super.getApplicationContext();
2595         }
2596 
wasGetApplicationContextCalled()2597         public boolean wasGetApplicationContextCalled() {
2598             return mGetApplicationContextWasCalled;
2599         }
2600     }
2601 
2602     /**
2603      * This should remain functionally equivalent to
2604      * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingUseApplicationContext. Modifications
2605      * to this test should be reflected in that test as necessary. See
2606      * http://go/modifying-webview-cts.
2607      */
2608     @Test
testStartSafeBrowsingUseApplicationContext()2609     public void testStartSafeBrowsingUseApplicationContext() throws Exception {
2610         final MockContext ctx =
2611                 new MockContext(
2612                         ApplicationProvider.getApplicationContext().getApplicationContext());
2613         final SettableFuture<Boolean> startSafeBrowsingFuture = SettableFuture.create();
2614         WebView.startSafeBrowsing(
2615                 ctx,
2616                 new ValueCallback<Boolean>() {
2617                     @Override
2618                     public void onReceiveValue(Boolean value) {
2619                         startSafeBrowsingFuture.set(ctx.wasGetApplicationContextCalled());
2620                         return;
2621                     }
2622                 });
2623         assertTrue(WebkitUtils.waitForFuture(startSafeBrowsingFuture));
2624     }
2625 
2626     /**
2627      * This should remain functionally equivalent to
2628      * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingWithNullCallbackDoesntCrash.
2629      * Modifications to this test should be reflected in that test as necessary. See
2630      * http://go/modifying-webview-cts.
2631      */
2632     @Test
testStartSafeBrowsingWithNullCallbackDoesntCrash()2633     public void testStartSafeBrowsingWithNullCallbackDoesntCrash() throws Exception {
2634         WebView.startSafeBrowsing(
2635                 ApplicationProvider.getApplicationContext().getApplicationContext(), null);
2636     }
2637 
2638     /**
2639      * This should remain functionally equivalent to
2640      * androidx.webkit.WebViewCompatTest#testStartSafeBrowsingInvokesCallback. Modifications to this
2641      * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
2642      */
2643     @Test
testStartSafeBrowsingInvokesCallback()2644     public void testStartSafeBrowsingInvokesCallback() throws Exception {
2645         final SettableFuture<Boolean> startSafeBrowsingFuture = SettableFuture.create();
2646         WebView.startSafeBrowsing(
2647                 ApplicationProvider.getApplicationContext().getApplicationContext(),
2648                 new ValueCallback<Boolean>() {
2649                     @Override
2650                     public void onReceiveValue(Boolean value) {
2651                         startSafeBrowsingFuture.set(Looper.getMainLooper().isCurrentThread());
2652                         return;
2653                     }
2654                 });
2655         assertTrue(WebkitUtils.waitForFuture(startSafeBrowsingFuture));
2656     }
2657 
savePrintedPage( final PrintDocumentAdapter adapter, final ParcelFileDescriptor descriptor, final PageRange[] pageRanges, final SettableFuture<Void> result)2658     private void savePrintedPage(
2659             final PrintDocumentAdapter adapter,
2660             final ParcelFileDescriptor descriptor,
2661             final PageRange[] pageRanges,
2662             final SettableFuture<Void> result) {
2663         adapter.onWrite(
2664                 pageRanges,
2665                 descriptor,
2666                 new CancellationSignal(),
2667                 new WriteResultCallback() {
2668                     @Override
2669                     public void onWriteFinished(PageRange[] pages) {
2670                         try {
2671                             descriptor.close();
2672                             result.set(null);
2673                         } catch (IOException ex) {
2674                             result.setException(ex);
2675                         }
2676                     }
2677                 });
2678     }
2679 
printDocumentStart(final PrintDocumentAdapter adapter)2680     private void printDocumentStart(final PrintDocumentAdapter adapter) {
2681         WebkitUtils.onMainThreadSync(
2682                 () -> {
2683                     adapter.onStart();
2684                 });
2685     }
2686 
printDocumentLayout( final PrintDocumentAdapter adapter, final PrintAttributes oldAttributes, final PrintAttributes newAttributes, final LayoutResultCallback layoutResultCallback)2687     private void printDocumentLayout(
2688             final PrintDocumentAdapter adapter,
2689             final PrintAttributes oldAttributes,
2690             final PrintAttributes newAttributes,
2691             final LayoutResultCallback layoutResultCallback) {
2692         WebkitUtils.onMainThreadSync(
2693                 () -> {
2694                     adapter.onLayout(
2695                             oldAttributes,
2696                             newAttributes,
2697                             new CancellationSignal(),
2698                             layoutResultCallback,
2699                             null);
2700                 });
2701     }
2702 
2703     private static class HrefCheckHandler extends Handler {
2704         private boolean mHadRecieved;
2705 
2706         private String mResultUrl;
2707 
HrefCheckHandler(Looper looper)2708         public HrefCheckHandler(Looper looper) {
2709             super(looper);
2710         }
2711 
hasCalledHandleMessage()2712         public boolean hasCalledHandleMessage() {
2713             return mHadRecieved;
2714         }
2715 
getResultUrl()2716         public String getResultUrl() {
2717             return mResultUrl;
2718         }
2719 
reset()2720         public void reset() {
2721             mResultUrl = null;
2722             mHadRecieved = false;
2723         }
2724 
2725         @Override
handleMessage(Message msg)2726         public void handleMessage(Message msg) {
2727             mResultUrl = msg.getData().getString("url");
2728             mHadRecieved = true;
2729         }
2730     }
2731 
moveFocusDown()2732     private void moveFocusDown() throws Throwable {
2733         // send down key and wait for idle
2734         getTestEnvironment().sendKeyDownUpSync(KeyEvent.KEYCODE_TAB);
2735         // waiting for idle isn't always sufficient for the key to be fully processed
2736         Thread.sleep(500);
2737     }
2738 
pollingCheckWebBackForwardList( final String currUrl, final int currIndex, final int size)2739     private void pollingCheckWebBackForwardList(
2740             final String currUrl, final int currIndex, final int size) {
2741         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
2742             @Override
2743             protected boolean check() {
2744                 WebBackForwardList list = mOnUiThread.copyBackForwardList();
2745                 return checkWebBackForwardList(list, currUrl, currIndex, size);
2746             }
2747         }.run();
2748     }
2749 
checkWebBackForwardList( WebBackForwardList list, String currUrl, int currIndex, int size)2750     private boolean checkWebBackForwardList(
2751             WebBackForwardList list, String currUrl, int currIndex, int size) {
2752         return (list != null)
2753                 && (list.getSize() == size)
2754                 && (list.getCurrentIndex() == currIndex)
2755                 && list.getItemAtIndex(currIndex).getUrl().equals(currUrl);
2756     }
2757 
assertGoBackOrForwardBySteps(boolean expected, int steps)2758     private void assertGoBackOrForwardBySteps(boolean expected, int steps) {
2759         // skip if steps equals to 0
2760         if (steps == 0) return;
2761 
2762         int start = steps > 0 ? 1 : steps;
2763         int end = steps > 0 ? steps : -1;
2764 
2765         // check all the steps in the history
2766         for (int i = start; i <= end; i++) {
2767             assertEquals(expected, mOnUiThread.canGoBackOrForward(i));
2768 
2769             // shortcut methods for one step
2770             if (i == 1) {
2771                 assertEquals(expected, mOnUiThread.canGoForward());
2772             } else if (i == -1) {
2773                 assertEquals(expected, mOnUiThread.canGoBack());
2774             }
2775         }
2776     }
2777 
isPictureFilledWithColor(Picture picture, int color)2778     private boolean isPictureFilledWithColor(Picture picture, int color) {
2779         if (picture.getWidth() == 0 || picture.getHeight() == 0) return false;
2780 
2781         Bitmap bitmap =
2782                 Bitmap.createBitmap(picture.getWidth(), picture.getHeight(), Config.ARGB_8888);
2783         picture.draw(new Canvas(bitmap));
2784 
2785         for (int i = 0; i < bitmap.getWidth(); i++) {
2786             for (int j = 0; j < bitmap.getHeight(); j++) {
2787                 if (color != bitmap.getPixel(i, j)) {
2788                     return false;
2789                 }
2790             }
2791         }
2792         return true;
2793     }
2794 
2795     /**
2796      * Waits at least MIN_SCROLL_WAIT_MS for scrolling to start. Once started, scrolling is checked
2797      * every SCROLL_WAIT_INTERVAL_MS for changes. Once changes have stopped, the function exits. If
2798      * no scrolling has happened then the function exits after MIN_SCROLL_WAIT milliseconds.
2799      *
2800      * @param previousScrollY The Y scroll position prior to waiting.
2801      */
waitForScrollingComplete(int previousScrollY)2802     private void waitForScrollingComplete(int previousScrollY) throws InterruptedException {
2803         int scrollY = previousScrollY;
2804         // wait at least MIN_SCROLL_WAIT for something to happen.
2805         long noChangeMinWait = SystemClock.uptimeMillis() + MIN_SCROLL_WAIT_MS;
2806         boolean scrollChanging = false;
2807         boolean scrollChanged = false;
2808         boolean minWaitExpired = false;
2809         while (scrollChanging || (!scrollChanged && !minWaitExpired)) {
2810             Thread.sleep(SCROLL_WAIT_INTERVAL_MS);
2811             int oldScrollY = scrollY;
2812             scrollY = mOnUiThread.getScrollY();
2813             scrollChanging = (scrollY != oldScrollY);
2814             scrollChanged = (scrollY != previousScrollY);
2815             minWaitExpired = (SystemClock.uptimeMillis() > noChangeMinWait);
2816         }
2817     }
2818 
2819     /**
2820      * This should remain functionally equivalent to
2821      * androidx.webkit.WebViewCompatTest#testGetSafeBrowsingPrivacyPolicyUrl. Modifications to this
2822      * test should be reflected in that test as necessary. See http://go/modifying-webview-cts.
2823      */
2824     @Test
testGetSafeBrowsingPrivacyPolicyUrl()2825     public void testGetSafeBrowsingPrivacyPolicyUrl() throws Exception {
2826         assertNotNull(WebView.getSafeBrowsingPrivacyPolicyUrl());
2827         try {
2828             new URL(WebView.getSafeBrowsingPrivacyPolicyUrl().toString());
2829         } catch (MalformedURLException e) {
2830             fail("The privacy policy URL should be a well-formed URL");
2831         }
2832     }
2833 
2834     @Test
testWebViewClassLoaderReturnsNonNull()2835     public void testWebViewClassLoaderReturnsNonNull() {
2836         assertNotNull(WebView.getWebViewClassLoader());
2837     }
2838 }
2839