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