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