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