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.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertTrue;
24 
25 import android.graphics.Bitmap;
26 import android.os.Message;
27 import android.platform.test.annotations.AppModeFull;
28 import android.util.Base64;
29 import android.view.ViewGroup;
30 import android.view.ViewParent;
31 import android.webkit.ConsoleMessage;
32 import android.webkit.JsPromptResult;
33 import android.webkit.JsResult;
34 import android.webkit.WebIconDatabase;
35 import android.webkit.WebSettings;
36 import android.webkit.WebView;
37 import android.webkit.cts.WebViewSyncLoader.WaitForProgressClient;
38 
39 import androidx.test.ext.junit.rules.ActivityScenarioRule;
40 import androidx.test.ext.junit.runners.AndroidJUnit4;
41 import androidx.test.filters.MediumTest;
42 
43 import com.android.compatibility.common.util.NullWebViewUtils;
44 import com.android.compatibility.common.util.PollingCheck;
45 
46 import com.google.common.util.concurrent.SettableFuture;
47 
48 import org.junit.After;
49 import org.junit.Assume;
50 import org.junit.Before;
51 import org.junit.Rule;
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 import java.util.concurrent.ArrayBlockingQueue;
56 import java.util.concurrent.BlockingQueue;
57 
58 @AppModeFull
59 @MediumTest
60 @RunWith(AndroidJUnit4.class)
61 public class WebChromeClientTest extends SharedWebViewTest{
62     private static final String JAVASCRIPT_UNLOAD = "javascript unload";
63     private static final String LISTENER_ADDED = "listener added";
64     private static final String TOUCH_RECEIVED = "touch received";
65 
66     @Rule
67     public ActivityScenarioRule mActivityScenarioRule =
68             new ActivityScenarioRule(WebViewCtsActivity.class);
69 
70     private SharedSdkWebServer mWebServer;
71     private WebIconDatabase mIconDb;
72     private WebViewOnUiThread mOnUiThread;
73     private boolean mBlockWindowCreationSync;
74     private boolean mBlockWindowCreationAsync;
75 
76     @Before
setUp()77     public void setUp() throws Exception {
78         WebView webview = getTestEnvironment().getWebView();
79         if (webview != null) {
80             mOnUiThread = new WebViewOnUiThread(webview);
81         }
82         mWebServer = getTestEnvironment().getSetupWebServer(SslMode.INSECURE);
83     }
84 
85     @After
tearDown()86     public void tearDown() throws Exception {
87         if (mOnUiThread != null) {
88             mOnUiThread.cleanUp();
89         }
90         if (mWebServer != null) {
91             mWebServer.shutdown();
92         }
93         if (mIconDb != null) {
94             mIconDb.removeAllIcons();
95             mIconDb.close();
96         }
97     }
98 
99     @Override
createTestEnvironment()100     protected SharedWebViewTestEnvironment createTestEnvironment() {
101         Assume.assumeTrue("WebView is not available", NullWebViewUtils.isWebViewAvailable());
102 
103         SharedWebViewTestEnvironment.Builder builder = new SharedWebViewTestEnvironment.Builder();
104 
105         mActivityScenarioRule
106                 .getScenario()
107                 .onActivity(
108                         activity -> {
109                             WebView webView = ((WebViewCtsActivity) activity).getWebView();
110                             builder.setHostAppInvoker(
111                                             SharedWebViewTestEnvironment.createHostAppInvoker(
112                                                     activity))
113                                     .setContext(activity)
114                                     .setWebView(webView)
115                                     .setRootLayout(((WebViewCtsActivity) activity).getRootLayout());
116                         });
117 
118         SharedWebViewTestEnvironment environment = builder.build();
119         return environment;
120     }
121 
122 
123     @Test
testOnProgressChanged()124     public void testOnProgressChanged() {
125         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
126         mOnUiThread.setWebChromeClient(webChromeClient);
127 
128         assertFalse(webChromeClient.hadOnProgressChanged());
129         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
130         mOnUiThread.loadUrlAndWaitForCompletion(url);
131 
132         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
133             @Override
134             protected boolean check() {
135                 return webChromeClient.hadOnProgressChanged();
136             }
137         }.run();
138     }
139 
140     @Test
testOnReceivedTitle()141     public void testOnReceivedTitle() throws Exception {
142         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
143         mOnUiThread.setWebChromeClient(webChromeClient);
144 
145         assertFalse(webChromeClient.hadOnReceivedTitle());
146         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
147         mOnUiThread.loadUrlAndWaitForCompletion(url);
148 
149         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
150             @Override
151             protected boolean check() {
152                 return webChromeClient.hadOnReceivedTitle();
153             }
154         }.run();
155         assertTrue(webChromeClient.hadOnReceivedTitle());
156         assertEquals(TestHtmlConstants.HELLO_WORLD_TITLE, webChromeClient.getPageTitle());
157     }
158 
159     @Test
testOnReceivedIcon()160     public void testOnReceivedIcon() throws Throwable {
161         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
162         mOnUiThread.setWebChromeClient(webChromeClient);
163 
164         WebkitUtils.onMainThreadSync(() -> {
165             // getInstance must run on the UI thread
166             mIconDb = WebIconDatabase.getInstance();
167             String dbPath = getTestEnvironment().getContext().getFilesDir().toString() + "/icons";
168             mIconDb.open(dbPath);
169         });
170         getTestEnvironment().waitForIdleSync();
171         Thread.sleep(100); // Wait for open to be received on the icon db thread.
172 
173         assertFalse(webChromeClient.hadOnReceivedIcon());
174         assertNull(mOnUiThread.getFavicon());
175 
176         String url = mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL);
177         mOnUiThread.loadUrlAndWaitForCompletion(url);
178 
179         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
180             @Override
181             protected boolean check() {
182                 return webChromeClient.hadOnReceivedIcon();
183             }
184         }.run();
185         assertNotNull(mOnUiThread.getFavicon());
186     }
187 
runWindowTest(boolean expectWindowClose)188     public void runWindowTest(boolean expectWindowClose) throws Exception {
189         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
190         mOnUiThread.setWebChromeClient(webChromeClient);
191 
192         final WebSettings settings = mOnUiThread.getSettings();
193         settings.setJavaScriptEnabled(true);
194         settings.setJavaScriptCanOpenWindowsAutomatically(true);
195         settings.setSupportMultipleWindows(true);
196 
197         assertFalse(webChromeClient.hadOnCreateWindow());
198 
199         // Load a page that opens a child window and sets a timeout after which the child
200         // will be closed.
201         mOnUiThread.loadUrlAndWaitForCompletion(mWebServer.
202                 getAssetUrl(TestHtmlConstants.JS_WINDOW_URL));
203 
204         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
205             @Override
206             protected boolean check() {
207                 return webChromeClient.hadOnCreateWindow();
208             }
209         }.run();
210 
211         if (expectWindowClose) {
212             new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
213                 @Override
214                 protected boolean check() {
215                     return webChromeClient.hadOnCloseWindow();
216                 }
217             }.run();
218         } else {
219             assertFalse(webChromeClient.hadOnCloseWindow());
220         }
221     }
222     @Test
testWindows()223     public void testWindows() throws Exception {
224         runWindowTest(true);
225     }
226 
227     @Test
testBlockWindowsSync()228     public void testBlockWindowsSync() throws Exception {
229         mBlockWindowCreationSync = true;
230         runWindowTest(false);
231     }
232 
233     @Test
testBlockWindowsAsync()234     public void testBlockWindowsAsync() throws Exception {
235         mBlockWindowCreationAsync = true;
236         runWindowTest(false);
237     }
238 
239     // Note that test is still a little flaky. See b/119468441.
240     @Test
testOnJsBeforeUnloadIsCalled()241     public void testOnJsBeforeUnloadIsCalled() throws Exception {
242         final WebSettings settings = mOnUiThread.getSettings();
243         settings.setJavaScriptEnabled(true);
244         settings.setJavaScriptCanOpenWindowsAutomatically(true);
245 
246         final BlockingQueue<String> pageTitleQueue = new ArrayBlockingQueue<>(3);
247         final SettableFuture<Void> onJsBeforeUnloadFuture = SettableFuture.create();
248         final MockWebChromeClient webChromeClientWaitTitle = new MockWebChromeClient() {
249             @Override
250             public void onReceivedTitle(WebView view, String title) {
251                 super.onReceivedTitle(view, title);
252                 pageTitleQueue.add(title);
253             }
254 
255             @Override
256             public boolean onJsBeforeUnload(
257                 WebView view, String url, String message, JsResult result) {
258                 boolean ret = super.onJsBeforeUnload(view, url, message, result);
259                 onJsBeforeUnloadFuture.set(null);
260                 return ret;
261             }
262         };
263         mOnUiThread.setWebChromeClient(webChromeClientWaitTitle);
264 
265         mOnUiThread.loadUrlAndWaitForCompletion(
266             mWebServer.getAssetUrl(TestHtmlConstants.JS_UNLOAD_URL));
267 
268         assertEquals(JAVASCRIPT_UNLOAD, WebkitUtils.waitForNextQueueElement(pageTitleQueue));
269         assertEquals(LISTENER_ADDED, WebkitUtils.waitForNextQueueElement(pageTitleQueue));
270         // Send a user gesture, required for unload to execute since WebView version 60.
271         tapWebView();
272         assertEquals(TOUCH_RECEIVED, WebkitUtils.waitForNextQueueElement(pageTitleQueue));
273 
274         // unload should trigger when we try to navigate away
275         mOnUiThread.loadUrlAndWaitForCompletion(
276             mWebServer.getAssetUrl(TestHtmlConstants.HELLO_WORLD_URL));
277 
278         WebkitUtils.waitForFuture(onJsBeforeUnloadFuture);
279     }
280 
281     @Test
testOnJsAlert()282     public void testOnJsAlert() throws Exception {
283         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
284         mOnUiThread.setWebChromeClient(webChromeClient);
285 
286         final WebSettings settings = mOnUiThread.getSettings();
287         settings.setJavaScriptEnabled(true);
288         settings.setJavaScriptCanOpenWindowsAutomatically(true);
289 
290         assertFalse(webChromeClient.hadOnJsAlert());
291 
292         String url = mWebServer.getAssetUrl(TestHtmlConstants.JS_ALERT_URL);
293         mOnUiThread.loadUrlAndWaitForCompletion(url);
294 
295         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
296             @Override
297             protected boolean check() {
298                 return webChromeClient.hadOnJsAlert();
299             }
300         }.run();
301         assertEquals(webChromeClient.getMessage(), "testOnJsAlert");
302     }
303 
304     @Test
testOnJsConfirm()305     public void testOnJsConfirm() throws Exception {
306         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
307         mOnUiThread.setWebChromeClient(webChromeClient);
308 
309         final WebSettings settings = mOnUiThread.getSettings();
310         settings.setJavaScriptEnabled(true);
311         settings.setJavaScriptCanOpenWindowsAutomatically(true);
312 
313         assertFalse(webChromeClient.hadOnJsConfirm());
314 
315         String url = mWebServer.getAssetUrl(TestHtmlConstants.JS_CONFIRM_URL);
316         mOnUiThread.loadUrlAndWaitForCompletion(url);
317 
318         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
319             @Override
320             protected boolean check() {
321                 return webChromeClient.hadOnJsConfirm();
322             }
323         }.run();
324         assertEquals(webChromeClient.getMessage(), "testOnJsConfirm");
325     }
326 
327     @Test
testOnJsPrompt()328     public void testOnJsPrompt() throws Exception {
329         final MockWebChromeClient webChromeClient = new MockWebChromeClient();
330         mOnUiThread.setWebChromeClient(webChromeClient);
331 
332         final WebSettings settings = mOnUiThread.getSettings();
333         settings.setJavaScriptEnabled(true);
334         settings.setJavaScriptCanOpenWindowsAutomatically(true);
335 
336         assertFalse(webChromeClient.hadOnJsPrompt());
337 
338         final String promptResult = "CTS";
339         webChromeClient.setPromptResult(promptResult);
340         String url = mWebServer.getAssetUrl(TestHtmlConstants.JS_PROMPT_URL);
341         mOnUiThread.loadUrlAndWaitForCompletion(url);
342 
343         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
344             @Override
345             protected boolean check() {
346                 return webChromeClient.hadOnJsPrompt();
347             }
348         }.run();
349         // the result returned by the client gets set as the page title
350         new PollingCheck(WebkitUtils.TEST_TIMEOUT_MS) {
351             @Override
352             protected boolean check() {
353                 return mOnUiThread.getTitle().equals(promptResult);
354             }
355         }.run();
356         assertEquals(webChromeClient.getMessage(), "testOnJsPrompt");
357     }
358 
359     @Test
testOnConsoleMessage()360     public void testOnConsoleMessage() throws Exception {
361         int numConsoleMessages = 4;
362         final BlockingQueue<ConsoleMessage> consoleMessageQueue =
363                 new ArrayBlockingQueue<>(numConsoleMessages);
364         final MockWebChromeClient webChromeClient = new MockWebChromeClient() {
365             @Override
366             public boolean onConsoleMessage(ConsoleMessage message) {
367                 consoleMessageQueue.add(message);
368                 // return false for default handling; i.e. printing the message.
369                 return false;
370             }
371         };
372         mOnUiThread.setWebChromeClient(webChromeClient);
373 
374         mOnUiThread.getSettings().setJavaScriptEnabled(true);
375         // Note: we assert line numbers, which are relative to the line in the HTML file. So, "\n"
376         // is significant in this test, and make sure to update consoleLineNumberOffset when
377         // editing the HTML.
378         final int consoleLineNumberOffset = 3;
379         final String unencodedHtml = "<html>\n"
380                 + "<script>\n"
381                 + "  console.log('message0');\n"
382                 + "  console.warn('message1');\n"
383                 + "  console.error('message2');\n"
384                 + "  console.info('message3');\n"
385                 + "</script>\n"
386                 + "</html>\n";
387         final String mimeType = null;
388         final String encoding = "base64";
389         String encodedHtml = Base64.encodeToString(unencodedHtml.getBytes(), Base64.NO_PADDING);
390         mOnUiThread.loadDataAndWaitForCompletion(encodedHtml, mimeType, encoding);
391 
392         // Expected message levels correspond to the order of the console messages defined above.
393         ConsoleMessage.MessageLevel[] expectedMessageLevels = {
394             ConsoleMessage.MessageLevel.LOG,
395             ConsoleMessage.MessageLevel.WARNING,
396             ConsoleMessage.MessageLevel.ERROR,
397             ConsoleMessage.MessageLevel.LOG,
398         };
399         for (int k = 0; k < numConsoleMessages; k++) {
400             final ConsoleMessage consoleMessage =
401                     WebkitUtils.waitForNextQueueElement(consoleMessageQueue);
402             final ConsoleMessage.MessageLevel expectedMessageLevel = expectedMessageLevels[k];
403             assertEquals("message " + k + " had wrong level",
404                     expectedMessageLevel,
405                     consoleMessage.messageLevel());
406             final String expectedMessage = "message" + k;
407             assertEquals("message " + k + " had wrong message",
408                     expectedMessage,
409                     consoleMessage.message());
410             final int expectedLineNumber = k + consoleLineNumberOffset;
411             assertEquals("message " + k + " had wrong line number",
412                     expectedLineNumber,
413                     consoleMessage.lineNumber());
414         }
415     }
416 
417     /**
418      * Taps in the the center of a webview.
419      */
tapWebView()420     private void tapWebView() {
421         int[] location = mOnUiThread.getLocationOnScreen();
422         int middleX = location[0] + mOnUiThread.getWebView().getWidth() / 2;
423         int middleY = location[1] + mOnUiThread.getWebView().getHeight() / 2;
424         getTestEnvironment().sendTapSync(middleX, middleY);
425 
426         // Wait for the system to process all events in the queue
427         getTestEnvironment().waitForIdleSync();
428     }
429 
430     private class MockWebChromeClient extends WaitForProgressClient {
431         private boolean mHadOnProgressChanged;
432         private boolean mHadOnReceivedTitle;
433         private String mPageTitle;
434         private boolean mHadOnJsAlert;
435         private boolean mHadOnJsConfirm;
436         private boolean mHadOnJsPrompt;
437         private boolean mHadOnJsBeforeUnload;
438         private String mMessage;
439         private String mPromptResult;
440         private boolean mHadOnCloseWindow;
441         private boolean mHadOnCreateWindow;
442         private boolean mHadOnRequestFocus;
443         private boolean mHadOnReceivedIcon;
444         private WebView mChildWebView;
445 
MockWebChromeClient()446         public MockWebChromeClient() {
447             super(mOnUiThread);
448         }
449 
setPromptResult(String promptResult)450         public void setPromptResult(String promptResult) {
451             mPromptResult = promptResult;
452         }
453 
hadOnProgressChanged()454         public boolean hadOnProgressChanged() {
455             return mHadOnProgressChanged;
456         }
457 
hadOnReceivedTitle()458         public boolean hadOnReceivedTitle() {
459             return mHadOnReceivedTitle;
460         }
461 
getPageTitle()462         public String getPageTitle() {
463             return mPageTitle;
464         }
465 
hadOnJsAlert()466         public boolean hadOnJsAlert() {
467             return mHadOnJsAlert;
468         }
469 
hadOnJsConfirm()470         public boolean hadOnJsConfirm() {
471             return mHadOnJsConfirm;
472         }
473 
hadOnJsPrompt()474         public boolean hadOnJsPrompt() {
475             return mHadOnJsPrompt;
476         }
477 
hadOnJsBeforeUnload()478         public boolean hadOnJsBeforeUnload() {
479             return mHadOnJsBeforeUnload;
480         }
481 
hadOnCreateWindow()482         public boolean hadOnCreateWindow() {
483             return mHadOnCreateWindow;
484         }
485 
hadOnCloseWindow()486         public boolean hadOnCloseWindow() {
487             return mHadOnCloseWindow;
488         }
489 
hadOnRequestFocus()490         public boolean hadOnRequestFocus() {
491             return mHadOnRequestFocus;
492         }
493 
hadOnReceivedIcon()494         public boolean hadOnReceivedIcon() {
495             return mHadOnReceivedIcon;
496         }
497 
getMessage()498         public String getMessage() {
499             return mMessage;
500         }
501 
502         @Override
onProgressChanged(WebView view, int newProgress)503         public void onProgressChanged(WebView view, int newProgress) {
504             super.onProgressChanged(view, newProgress);
505             mHadOnProgressChanged = true;
506         }
507 
508         @Override
onReceivedTitle(WebView view, String title)509         public void onReceivedTitle(WebView view, String title) {
510             super.onReceivedTitle(view, title);
511             mPageTitle = title;
512             mHadOnReceivedTitle = true;
513         }
514 
515         @Override
onJsAlert(WebView view, String url, String message, JsResult result)516         public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
517             super.onJsAlert(view, url, message, result);
518             mHadOnJsAlert = true;
519             mMessage = message;
520             result.confirm();
521             return true;
522         }
523 
524         @Override
onJsConfirm(WebView view, String url, String message, JsResult result)525         public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
526             super.onJsConfirm(view, url, message, result);
527             mHadOnJsConfirm = true;
528             mMessage = message;
529             result.confirm();
530             return true;
531         }
532 
533         @Override
onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result)534         public boolean onJsPrompt(WebView view, String url, String message,
535                 String defaultValue, JsPromptResult result) {
536             super.onJsPrompt(view, url, message, defaultValue, result);
537             mHadOnJsPrompt = true;
538             mMessage = message;
539             result.confirm(mPromptResult);
540             return true;
541         }
542 
543         @Override
onJsBeforeUnload(WebView view, String url, String message, JsResult result)544         public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {
545             super.onJsBeforeUnload(view, url, message, result);
546             mHadOnJsBeforeUnload = true;
547             mMessage = message;
548             result.confirm();
549             return true;
550         }
551 
552         @Override
onCloseWindow(WebView window)553         public void onCloseWindow(WebView window) {
554             super.onCloseWindow(window);
555             mHadOnCloseWindow = true;
556 
557             if (mChildWebView != null) {
558                 ViewParent parent =  mChildWebView.getParent();
559                 if (parent instanceof ViewGroup) {
560                     ((ViewGroup) parent).removeView(mChildWebView);
561                 }
562                 mChildWebView.destroy();
563             }
564 
565         }
566 
567         @Override
onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg)568         public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture,
569                 Message resultMsg) {
570             mHadOnCreateWindow = true;
571             if (mBlockWindowCreationSync) {
572                 return false;
573             }
574             WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
575             if (mBlockWindowCreationAsync) {
576                 transport.setWebView(null);
577             } else {
578                 mChildWebView = new WebView(getTestEnvironment().getContext());
579                 final WebSettings settings = mChildWebView.getSettings();
580                 settings.setJavaScriptEnabled(true);
581                 mChildWebView.setWebChromeClient(this);
582                 transport.setWebView(mChildWebView);
583                 getTestEnvironment().addContentView(mChildWebView, new ViewGroup.LayoutParams(
584                         ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
585             }
586             resultMsg.sendToTarget();
587             return true;
588         }
589 
590         @Override
onRequestFocus(WebView view)591         public void onRequestFocus(WebView view) {
592             mHadOnRequestFocus = true;
593         }
594 
595         @Override
onReceivedIcon(WebView view, Bitmap icon)596         public void onReceivedIcon(WebView view, Bitmap icon) {
597             mHadOnReceivedIcon = true;
598         }
599     }
600 }
601