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