1 // Copyright 2014 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.android_webview.test; 6 7 import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout; 8 9 import android.os.Handler; 10 import android.os.HandlerThread; 11 import android.os.Looper; 12 import android.support.test.InstrumentationRegistry; 13 import android.webkit.JavascriptInterface; 14 15 import androidx.test.filters.SmallTest; 16 17 import org.hamcrest.Matchers; 18 import org.junit.After; 19 import org.junit.Assert; 20 import org.junit.Before; 21 import org.junit.Rule; 22 import org.junit.Test; 23 import org.junit.runner.RunWith; 24 25 import org.chromium.android_webview.AwContents; 26 import org.chromium.android_webview.test.util.CommonResources; 27 import org.chromium.base.test.util.Criteria; 28 import org.chromium.base.test.util.CriteriaHelper; 29 import org.chromium.base.test.util.Feature; 30 import org.chromium.content_public.browser.MessagePort; 31 import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper; 32 import org.chromium.content_public.browser.test.util.TestThreadUtils; 33 import org.chromium.net.test.util.TestWebServer; 34 35 import java.util.concurrent.CountDownLatch; 36 import java.util.concurrent.LinkedBlockingQueue; 37 import java.util.concurrent.TimeUnit; 38 39 /** 40 * The tests for content postMessage API. 41 */ 42 @RunWith(AwJUnit4ClassRunner.class) 43 public class PostMessageTest { 44 @Rule 45 public AwActivityTestRule mActivityTestRule = new AwActivityTestRule(); 46 47 private static final String SOURCE_ORIGIN = ""; 48 // Timeout to failure, in milliseconds 49 private static final long TIMEOUT = scaleTimeout(5000); 50 51 // Inject to the page to verify received messages. 52 private static class MessageObject { 53 private LinkedBlockingQueue<Data> mQueue = new LinkedBlockingQueue<>(); 54 55 public static class Data { 56 public String mMessage; 57 public String mOrigin; 58 public int[] mPorts; 59 Data(String message, String origin, int[] ports)60 public Data(String message, String origin, int[] ports) { 61 mMessage = message; 62 mOrigin = origin; 63 mPorts = ports; 64 } 65 } 66 67 @JavascriptInterface setMessageParams(String message, String origin, int[] ports)68 public void setMessageParams(String message, String origin, int[] ports) { 69 mQueue.add(new Data(message, origin, ports)); 70 } 71 waitForMessage()72 public Data waitForMessage() throws Exception { 73 return AwActivityTestRule.waitForNextQueueElement(mQueue); 74 } 75 } 76 77 private static class ChannelContainer { 78 private MessagePort[] mChannel; 79 private LinkedBlockingQueue<Data> mQueue = new LinkedBlockingQueue<>(); 80 81 public static class Data { 82 public String mMessage; 83 public Looper mLastLooper; 84 Data(String message, Looper looper)85 public Data(String message, Looper looper) { 86 mMessage = message; 87 mLastLooper = looper; 88 } 89 } 90 set(MessagePort[] channel)91 public void set(MessagePort[] channel) { 92 mChannel = channel; 93 } 94 get()95 public MessagePort[] get() { 96 return mChannel; 97 } 98 notifyCalled(String message)99 public void notifyCalled(String message) { 100 try { 101 mQueue.add(new Data(message, Looper.myLooper())); 102 } catch (IllegalStateException e) { 103 // We expect this add operation will always succeed since the default capacity of 104 // the queue is Integer.MAX_VALUE. 105 } 106 } 107 waitForMessageCallback()108 public Data waitForMessageCallback() throws Exception { 109 return AwActivityTestRule.waitForNextQueueElement(mQueue); 110 } 111 isQueueEmpty()112 public boolean isQueueEmpty() { 113 return mQueue.isEmpty(); 114 } 115 } 116 117 private MessageObject mMessageObject; 118 private TestAwContentsClient mContentsClient; 119 private AwTestContainerView mTestContainerView; 120 private AwContents mAwContents; 121 private TestWebServer mWebServer; 122 123 @Before setUp()124 public void setUp() throws Exception { 125 mMessageObject = new MessageObject(); 126 mContentsClient = new TestAwContentsClient(); 127 mTestContainerView = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient); 128 mAwContents = mTestContainerView.getAwContents(); 129 AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents); 130 131 try { 132 AwActivityTestRule.addJavascriptInterfaceOnUiThread( 133 mAwContents, mMessageObject, "messageObject"); 134 } catch (Throwable t) { 135 throw new RuntimeException(t); 136 } 137 mWebServer = TestWebServer.start(); 138 } 139 140 @After tearDown()141 public void tearDown() { 142 mWebServer.shutdown(); 143 } 144 145 private static final String WEBVIEW_MESSAGE = "from_webview"; 146 private static final String JS_MESSAGE = "from_js"; 147 148 private static final String TEST_PAGE = 149 "<!DOCTYPE html><html><body>" 150 + " <script>" 151 + " onmessage = function (e) {" 152 + " messageObject.setMessageParams(e.data, e.origin, e.ports);" 153 + " if (e.ports != null && e.ports.length > 0) {" 154 + " e.ports[0].postMessage(\"" + JS_MESSAGE + "\");" 155 + " }" 156 + " }" 157 + " </script>" 158 + "</body></html>"; 159 160 // Concats all the data fields of the received messages and makes it 161 // available as page title. 162 private static final String TITLE_FROM_POSTMESSAGE_TO_FRAME = 163 "<!DOCTYPE html><html><body>" 164 + " <script>" 165 + " var received = '';" 166 + " onmessage = function (e) {" 167 + " received += e.data;" 168 + " document.title = received;" 169 + " }" 170 + " </script>" 171 + "</body></html>"; 172 173 // Concats all the data fields of the received messages to the transferred channel 174 // and makes it available as page title. 175 private static final String TITLE_FROM_POSTMESSAGE_TO_CHANNEL = 176 "<!DOCTYPE html><html><body>" 177 + " <script>" 178 + " var received = '';" 179 + " onmessage = function (e) {" 180 + " var myport = e.ports[0];" 181 + " myport.onmessage = function (f) {" 182 + " received += f.data;" 183 + " document.title = received;" 184 + " }" 185 + " }" 186 + " </script>" 187 + "</body></html>"; 188 189 // Call on non-UI thread. expectTitle(String title)190 private void expectTitle(String title) { 191 CriteriaHelper.pollUiThread( 192 () -> Criteria.checkThat(mAwContents.getTitle(), Matchers.is(title))); 193 } 194 loadPage(String page)195 private void loadPage(String page) throws Throwable { 196 final String url = mWebServer.setResponse("/test.html", page, 197 CommonResources.getTextHtmlHeaders(true)); 198 OnPageFinishedHelper onPageFinishedHelper = mContentsClient.getOnPageFinishedHelper(); 199 int currentCallCount = onPageFinishedHelper.getCallCount(); 200 mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url); 201 onPageFinishedHelper.waitForCallback(currentCallCount); 202 } 203 204 @Test 205 @SmallTest 206 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToMainFrame()207 public void testPostMessageToMainFrame() throws Throwable { 208 verifyPostMessageToMainFrame(mWebServer.getBaseUrl()); 209 } 210 211 @Test 212 @SmallTest 213 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToMainFrameUsingWildcard()214 public void testPostMessageToMainFrameUsingWildcard() throws Throwable { 215 verifyPostMessageToMainFrame("*"); 216 } 217 218 @Test 219 @SmallTest 220 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToMainFrameUsingEmptyStringAsWildcard()221 public void testPostMessageToMainFrameUsingEmptyStringAsWildcard() throws Throwable { 222 verifyPostMessageToMainFrame(""); 223 } 224 verifyPostMessageToMainFrame(final String targetOrigin)225 private void verifyPostMessageToMainFrame(final String targetOrigin) throws Throwable { 226 loadPage(TEST_PAGE); 227 InstrumentationRegistry.getInstrumentation().runOnMainSync( 228 () -> mAwContents.postMessageToMainFrame(WEBVIEW_MESSAGE, targetOrigin, null)); 229 MessageObject.Data data = mMessageObject.waitForMessage(); 230 Assert.assertEquals(WEBVIEW_MESSAGE, data.mMessage); 231 Assert.assertEquals(SOURCE_ORIGIN, data.mOrigin); 232 } 233 234 @Test 235 @SmallTest 236 @Feature({"AndroidWebView", "Android-PostMessage"}) testTransferringSamePortTwiceViaPostMessageToMainFrameNotAllowed()237 public void testTransferringSamePortTwiceViaPostMessageToMainFrameNotAllowed() 238 throws Throwable { 239 loadPage(TEST_PAGE); 240 final CountDownLatch latch = new CountDownLatch(1); 241 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 242 MessagePort[] channel = mAwContents.createMessageChannel(); 243 mAwContents.postMessageToMainFrame( 244 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 245 // Retransfer the port. This should fail with an exception. 246 try { 247 mAwContents.postMessageToMainFrame( 248 "2", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 249 } catch (IllegalStateException ex) { 250 latch.countDown(); 251 return; 252 } 253 Assert.fail(); 254 }); 255 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 256 } 257 258 // There are two cases that put a port in a started state. 259 // 1. posting a message 260 // 2. setting an event handler. 261 // A started port cannot return to "non-started" state. The four tests below verifies 262 // these conditions for both conditions, using message ports and message channels. 263 @Test 264 @SmallTest 265 @Feature({"AndroidWebView", "Android-PostMessage"}) testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1()266 public void testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1() throws Throwable { 267 loadPage(TEST_PAGE); 268 final CountDownLatch latch = new CountDownLatch(1); 269 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 270 MessagePort[] channel = mAwContents.createMessageChannel(); 271 channel[1].postMessage("1", null); 272 try { 273 mAwContents.postMessageToMainFrame( 274 "2", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 275 } catch (IllegalStateException ex) { 276 latch.countDown(); 277 return; 278 } 279 Assert.fail(); 280 }); 281 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 282 } 283 284 // see documentation in testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1 285 @Test 286 @SmallTest 287 @Feature({"AndroidWebView", "Android-PostMessage"}) testStartedPortCannotBeTransferredUsingPostMessageToMainFrame2()288 public void testStartedPortCannotBeTransferredUsingPostMessageToMainFrame2() throws Throwable { 289 loadPage(TEST_PAGE); 290 final CountDownLatch latch = new CountDownLatch(1); 291 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 292 MessagePort[] channel = mAwContents.createMessageChannel(); 293 // set a web event handler, this puts the port in a started state. 294 channel[1].setMessageCallback((message, sentPorts) -> { 295 }, null); 296 try { 297 mAwContents.postMessageToMainFrame( 298 "2", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 299 } catch (IllegalStateException ex) { 300 latch.countDown(); 301 return; 302 } 303 Assert.fail(); 304 }); 305 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 306 } 307 308 // see documentation in testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1 309 @Test 310 @SmallTest 311 @Feature({"AndroidWebView", "Android-PostMessage"}) testStartedPortCannotBeTransferredUsingMessageChannel1()312 public void testStartedPortCannotBeTransferredUsingMessageChannel1() throws Throwable { 313 loadPage(TEST_PAGE); 314 final CountDownLatch latch = new CountDownLatch(1); 315 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 316 MessagePort[] channel1 = mAwContents.createMessageChannel(); 317 channel1[1].postMessage("1", null); 318 MessagePort[] channel2 = mAwContents.createMessageChannel(); 319 try { 320 channel2[0].postMessage("2", new MessagePort[] {channel1[1]}); 321 } catch (IllegalStateException ex) { 322 latch.countDown(); 323 return; 324 } 325 Assert.fail(); 326 }); 327 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 328 } 329 330 // see documentation in testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1 331 @Test 332 @SmallTest 333 @Feature({"AndroidWebView", "Android-PostMessage"}) testStartedPortCannotBeTransferredUsingMessageChannel2()334 public void testStartedPortCannotBeTransferredUsingMessageChannel2() throws Throwable { 335 loadPage(TEST_PAGE); 336 final CountDownLatch latch = new CountDownLatch(1); 337 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 338 MessagePort[] channel1 = mAwContents.createMessageChannel(); 339 // set a web event handler, this puts the port in a started state. 340 channel1[1].setMessageCallback((message, sentPorts) -> { 341 }, null); 342 MessagePort[] channel2 = mAwContents.createMessageChannel(); 343 try { 344 channel2[0].postMessage("1", new MessagePort[] {channel1[1]}); 345 } catch (IllegalStateException ex) { 346 latch.countDown(); 347 return; 348 } 349 Assert.fail(); 350 }); 351 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 352 } 353 354 355 // channel[0] and channel[1] are entangled ports, establishing a channel. Verify 356 // it is not allowed to transfer channel[0] on channel[0].postMessage. 357 // TODO(sgurun) Note that the related case of posting channel[1] via 358 // channel[0].postMessage does not throw a JS exception at present. We do not throw 359 // an exception in this case either since the information of entangled port is not 360 // available at the source port. We need a new mechanism to implement to prevent 361 // this case. 362 @Test 363 @SmallTest 364 @Feature({"AndroidWebView", "Android-PostMessage"}) testTransferringSourcePortViaMessageChannelNotAllowed()365 public void testTransferringSourcePortViaMessageChannelNotAllowed() throws Throwable { 366 loadPage(TEST_PAGE); 367 final CountDownLatch latch = new CountDownLatch(1); 368 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 369 MessagePort[] channel = mAwContents.createMessageChannel(); 370 try { 371 channel[0].postMessage("1", new MessagePort[] {channel[0]}); 372 } catch (IllegalStateException ex) { 373 latch.countDown(); 374 return; 375 } 376 Assert.fail(); 377 }); 378 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 379 } 380 381 // Verify a closed port cannot be transferred to a frame. 382 @Test 383 @SmallTest 384 @Feature({"AndroidWebView", "Android-PostMessage"}) testSendClosedPortToFrameNotAllowed()385 public void testSendClosedPortToFrameNotAllowed() throws Throwable { 386 loadPage(TEST_PAGE); 387 final CountDownLatch latch = new CountDownLatch(1); 388 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 389 MessagePort[] channel = mAwContents.createMessageChannel(); 390 channel[1].close(); 391 try { 392 mAwContents.postMessageToMainFrame( 393 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 394 } catch (IllegalStateException ex) { 395 latch.countDown(); 396 return; 397 } 398 Assert.fail(); 399 }); 400 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 401 } 402 403 // Verify a closed port cannot be transferred to a port. 404 @Test 405 @SmallTest 406 @Feature({"AndroidWebView", "Android-PostMessage"}) testSendClosedPortToPortNotAllowed()407 public void testSendClosedPortToPortNotAllowed() throws Throwable { 408 loadPage(TEST_PAGE); 409 final CountDownLatch latch = new CountDownLatch(1); 410 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 411 MessagePort[] channel1 = mAwContents.createMessageChannel(); 412 MessagePort[] channel2 = mAwContents.createMessageChannel(); 413 channel2[1].close(); 414 try { 415 channel1[0].postMessage("1", new MessagePort[] {channel2[1]}); 416 } catch (IllegalStateException ex) { 417 latch.countDown(); 418 return; 419 } 420 Assert.fail(); 421 }); 422 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 423 } 424 425 // Verify messages cannot be posted to closed ports. 426 @Test 427 @SmallTest 428 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToClosedPortNotAllowed()429 public void testPostMessageToClosedPortNotAllowed() throws Throwable { 430 loadPage(TEST_PAGE); 431 final CountDownLatch latch = new CountDownLatch(1); 432 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 433 MessagePort[] channel = mAwContents.createMessageChannel(); 434 channel[0].close(); 435 try { 436 channel[0].postMessage("1", null); 437 } catch (IllegalStateException ex) { 438 latch.countDown(); 439 return; 440 } 441 Assert.fail(); 442 }); 443 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 444 } 445 446 // Verify messages posted before closing a port is received at the destination port. 447 @Test 448 @SmallTest 449 @Feature({"AndroidWebView", "Android-PostMessage"}) testMessagesPostedBeforeClosingPortAreTransferred()450 public void testMessagesPostedBeforeClosingPortAreTransferred() throws Throwable { 451 loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL); 452 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 453 MessagePort[] channel = mAwContents.createMessageChannel(); 454 mAwContents.postMessageToMainFrame( 455 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 456 channel[0].postMessage("2", null); 457 channel[0].postMessage("3", null); 458 channel[0].close(); 459 }); 460 expectTitle("23"); 461 } 462 463 // Verify a transferred port using postMessageToMainFrame cannot be closed. 464 @Test 465 @SmallTest 466 @Feature({"AndroidWebView", "Android-PostMessage"}) testClosingTransferredPortToFrameThrowsAnException()467 public void testClosingTransferredPortToFrameThrowsAnException() throws Throwable { 468 loadPage(TEST_PAGE); 469 final CountDownLatch latch = new CountDownLatch(1); 470 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 471 MessagePort[] channel = mAwContents.createMessageChannel(); 472 mAwContents.postMessageToMainFrame( 473 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 474 try { 475 channel[1].close(); 476 } catch (IllegalStateException ex) { 477 latch.countDown(); 478 return; 479 } 480 Assert.fail(); 481 }); 482 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 483 } 484 485 // Verify a transferred port using postMessageToMainFrame cannot be closed. 486 @Test 487 @SmallTest 488 @Feature({"AndroidWebView", "Android-PostMessage"}) testClosingTransferredPortToChannelThrowsAnException()489 public void testClosingTransferredPortToChannelThrowsAnException() throws Throwable { 490 loadPage(TEST_PAGE); 491 final CountDownLatch latch = new CountDownLatch(1); 492 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 493 MessagePort[] channel1 = mAwContents.createMessageChannel(); 494 mAwContents.postMessageToMainFrame( 495 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel1[1]}); 496 MessagePort[] channel2 = mAwContents.createMessageChannel(); 497 channel1[0].postMessage("2", new MessagePort[] {channel2[0]}); 498 try { 499 channel2[0].close(); 500 } catch (IllegalStateException ex) { 501 latch.countDown(); 502 return; 503 } 504 Assert.fail(); 505 }); 506 boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS); 507 } 508 509 // Create two message channels, and while they are in pending state, transfer the 510 // second one in the first one. 511 @Test 512 @SmallTest 513 @Feature({"AndroidWebView", "Android-PostMessage"}) testPendingPortCanBeTransferredInPendingPort()514 public void testPendingPortCanBeTransferredInPendingPort() throws Throwable { 515 loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL); 516 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 517 MessagePort[] channel1 = mAwContents.createMessageChannel(); 518 mAwContents.postMessageToMainFrame( 519 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel1[1]}); 520 MessagePort[] channel2 = mAwContents.createMessageChannel(); 521 channel1[0].postMessage("2", new MessagePort[] {channel2[0]}); 522 }); 523 expectTitle("2"); 524 } 525 526 private static final String ECHO_PAGE = 527 "<!DOCTYPE html><html><body>" 528 + " <script>" 529 + " onmessage = function (e) {" 530 + " var myPort = e.ports[0];" 531 + " myPort.onmessage = function(e) {" 532 + " myPort.postMessage(e.data + \"" + JS_MESSAGE + "\"); }" 533 + " }" 534 + " </script>" 535 + "</body></html>"; 536 537 private static final String HELLO = "HELLO"; 538 539 // Message channels are created on UI thread. Verify that a message port 540 // can be transferred to JS and full communication can happen on it. Do 541 // this by sending a message to JS and letting it echo the message with 542 // some text prepended to it. 543 @Test 544 @SmallTest 545 @Feature({"AndroidWebView", "Android-PostMessage"}) testMessageChannelUsingInitializedPort()546 public void testMessageChannelUsingInitializedPort() throws Throwable { 547 final ChannelContainer channelContainer = new ChannelContainer(); 548 loadPage(ECHO_PAGE); 549 final MessagePort[] channel = 550 TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel()); 551 552 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 553 channel[0].setMessageCallback( 554 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 555 mAwContents.postMessageToMainFrame( 556 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 557 channel[0].postMessage(HELLO, null); 558 }); 559 // wait for the asynchronous response from JS 560 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 561 Assert.assertEquals(HELLO + JS_MESSAGE, data.mMessage); 562 } 563 564 // Verify that a message port can be used immediately (even if it is in 565 // pending state) after creation. In particular make sure the message port can be 566 // transferred to JS and full communication can happen on it. 567 // Do this by sending a message to JS and let it echo'ing the message with 568 // some text prepended to it. 569 @SmallTest 570 @Feature({"AndroidWebView", "Android-PostMessage"}) 571 @Test testMessageChannelUsingPendingPort()572 public void testMessageChannelUsingPendingPort() throws Throwable { 573 final ChannelContainer channelContainer = new ChannelContainer(); 574 loadPage(ECHO_PAGE); 575 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 576 MessagePort[] channel = mAwContents.createMessageChannel(); 577 channel[0].setMessageCallback( 578 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 579 mAwContents.postMessageToMainFrame( 580 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 581 channel[0].postMessage(HELLO, null); 582 }); 583 // Wait for the asynchronous response from JS. 584 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 585 Assert.assertEquals(HELLO + JS_MESSAGE, data.mMessage); 586 } 587 588 // Verify that a message port can be used for message transfer when both 589 // ports are owned by same Webview. 590 @Test 591 @SmallTest 592 @Feature({"AndroidWebView", "Android-PostMessage"}) testMessageChannelCommunicationWithinWebView()593 public void testMessageChannelCommunicationWithinWebView() throws Throwable { 594 final ChannelContainer channelContainer = new ChannelContainer(); 595 loadPage(ECHO_PAGE); 596 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 597 MessagePort[] channel = mAwContents.createMessageChannel(); 598 channel[1].setMessageCallback( 599 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 600 channel[0].postMessage(HELLO, null); 601 }); 602 // Wait for the asynchronous response from JS. 603 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 604 Assert.assertEquals(HELLO, data.mMessage); 605 } 606 607 // Post a message with a pending port to a frame and then post a bunch of messages 608 // after that. Make sure that they are not ordered at the receiver side. 609 @Test 610 @SmallTest 611 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToMainFrameNotReordersMessages()612 public void testPostMessageToMainFrameNotReordersMessages() throws Throwable { 613 loadPage(TITLE_FROM_POSTMESSAGE_TO_FRAME); 614 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 615 MessagePort[] channel = mAwContents.createMessageChannel(); 616 mAwContents.postMessageToMainFrame( 617 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 618 mAwContents.postMessageToMainFrame("2", mWebServer.getBaseUrl(), null); 619 mAwContents.postMessageToMainFrame("3", mWebServer.getBaseUrl(), null); 620 }); 621 expectTitle("123"); 622 } 623 624 private static final String RECEIVE_JS_MESSAGE_CHANNEL_PAGE = 625 "<!DOCTYPE html><html><body>" 626 + " <script>" 627 + " var received ='';" 628 + " var mc = new MessageChannel();" 629 + " mc.port1.onmessage = function (e) {" 630 + " received += e.data;" 631 + " document.title = received;" 632 + " if (e.data == '2') { mc.port1.postMessage('3'); }" 633 + " };" 634 + " onmessage = function (e) {" 635 + " var myPort = e.ports[0];" 636 + " myPort.postMessage('from window', [mc.port2]);" 637 + " }" 638 + " </script>" 639 + "</body></html>"; 640 641 // Test webview can use a message port received from JS for full duplex communication. 642 // Test steps: 643 // 1. Java creates a message channel, and send one port to JS 644 // 2. JS creates a new message channel and sends one port to Java using the channel in 1 645 // 3. Java sends a message using the new channel in 2. 646 // 4. Js responds to this message using the channel in 2. 647 // 5. Java responds to message in 4 using the channel in 2. 648 @SmallTest 649 @Feature({"AndroidWebView", "Android-PostMessage"}) 650 @Test testCanUseReceivedAwMessagePortFromJS()651 public void testCanUseReceivedAwMessagePortFromJS() throws Throwable { 652 loadPage(RECEIVE_JS_MESSAGE_CHANNEL_PAGE); 653 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 654 MessagePort[] channel = mAwContents.createMessageChannel(); 655 mAwContents.postMessageToMainFrame( 656 "1", mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 657 channel[0].setMessageCallback((message, p) -> { 658 p[0].setMessageCallback((message1, q) -> { 659 Assert.assertEquals("3", message1); 660 p[0].postMessage("4", null); 661 }, null); 662 p[0].postMessage("2", null); 663 }, null); 664 }); 665 expectTitle("24"); 666 } 667 668 private static final String WORKER_MESSAGE = "from_worker"; 669 670 // Listen for messages. Pass port 1 to worker and use port 2 to receive messages from 671 // from worker. 672 private static final String TEST_PAGE_FOR_PORT_TRANSFER = 673 "<!DOCTYPE html><html><body>" 674 + " <script>" 675 + " var worker = new Worker(\"worker.js\");" 676 + " onmessage = function (e) {" 677 + " if (e.data == \"" + WEBVIEW_MESSAGE + "\") {" 678 + " worker.postMessage(\"worker_port\", [e.ports[0]]);" 679 + " var messageChannelPort = e.ports[1];" 680 + " messageChannelPort.onmessage = receiveWorkerMessage;" 681 + " }" 682 + " };" 683 + " function receiveWorkerMessage(e) {" 684 + " if (e.data == \"" + WORKER_MESSAGE + "\") {" 685 + " messageObject.setMessageParams(e.data, e.origin, e.ports);" 686 + " }" 687 + " };" 688 + " </script>" 689 + "</body></html>"; 690 691 private static final String WORKER_SCRIPT = 692 "onmessage = function(e) {" 693 + " if (e.data == \"worker_port\") {" 694 + " var toWindow = e.ports[0];" 695 + " toWindow.postMessage(\"" + WORKER_MESSAGE + "\");" 696 + " toWindow.start();" 697 + " }" 698 + "}"; 699 700 // Test if message ports created at the native side can be transferred 701 // to JS side, to establish a communication channel between a worker and a frame. 702 @Test 703 @SmallTest 704 @Feature({"AndroidWebView", "Android-PostMessage"}) testTransferPortsToWorker()705 public void testTransferPortsToWorker() throws Throwable { 706 mWebServer.setResponse("/worker.js", WORKER_SCRIPT, 707 CommonResources.getTextJavascriptHeaders(true)); 708 loadPage(TEST_PAGE_FOR_PORT_TRANSFER); 709 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 710 MessagePort[] channel = mAwContents.createMessageChannel(); 711 mAwContents.postMessageToMainFrame(WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), 712 new MessagePort[] {channel[0], channel[1]}); 713 }); 714 MessageObject.Data data = mMessageObject.waitForMessage(); 715 Assert.assertEquals(WORKER_MESSAGE, data.mMessage); 716 } 717 718 private static final String POPUP_MESSAGE = "from_popup"; 719 private static final String POPUP_URL = "/popup.html"; 720 private static final String IFRAME_URL = "/iframe.html"; 721 private static final String MAIN_PAGE_FOR_POPUP_TEST = "<!DOCTYPE html><html>" 722 + "<head>" 723 + " <script>" 724 + " function createPopup() {" 725 + " var popupWindow = window.open('" + POPUP_URL + "');" 726 + " onmessage = function(e) {" 727 + " popupWindow.postMessage(e.data, '*', e.ports);" 728 + " };" 729 + " }" 730 + " </script>" 731 + "</head>" 732 + "</html>"; 733 734 // Sends message and ports to the iframe. 735 private static final String POPUP_PAGE_WITH_IFRAME = "<!DOCTYPE html><html>" 736 + "<script>" 737 + " onmessage = function(e) {" 738 + " var iframe = document.getElementsByTagName('iframe')[0];" 739 + " iframe.contentWindow.postMessage('" + POPUP_MESSAGE + "', '*', e.ports);" 740 + " };" 741 + "</script>" 742 + "<body><iframe src='" + IFRAME_URL + "'></iframe></body>" 743 + "</html>"; 744 745 // Test if WebView can post a message from/to a popup window owning a message port. 746 @Test 747 @SmallTest 748 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToPopup()749 public void testPostMessageToPopup() throws Throwable { 750 mActivityTestRule.triggerPopup(mAwContents, mContentsClient, mWebServer, 751 MAIN_PAGE_FOR_POPUP_TEST, ECHO_PAGE, POPUP_URL, "createPopup()"); 752 mActivityTestRule.connectPendingPopup(mAwContents); 753 final ChannelContainer channelContainer = new ChannelContainer(); 754 755 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 756 MessagePort[] channel = mAwContents.createMessageChannel(); 757 channel[0].setMessageCallback( 758 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 759 mAwContents.postMessageToMainFrame( 760 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 761 channel[0].postMessage(HELLO, null); 762 }); 763 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 764 Assert.assertEquals(HELLO + JS_MESSAGE, data.mMessage); 765 } 766 767 // Test if WebView can post a message from/to an iframe in a popup window. 768 @Test 769 @SmallTest 770 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageToIframeInsidePopup()771 public void testPostMessageToIframeInsidePopup() throws Throwable { 772 mWebServer.setResponse(IFRAME_URL, ECHO_PAGE, null); 773 mActivityTestRule.triggerPopup(mAwContents, mContentsClient, mWebServer, 774 MAIN_PAGE_FOR_POPUP_TEST, POPUP_PAGE_WITH_IFRAME, POPUP_URL, "createPopup()"); 775 mActivityTestRule.connectPendingPopup(mAwContents); 776 final ChannelContainer channelContainer = new ChannelContainer(); 777 778 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 779 MessagePort[] channel = mAwContents.createMessageChannel(); 780 channel[0].setMessageCallback( 781 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 782 mAwContents.postMessageToMainFrame( 783 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 784 channel[0].postMessage(HELLO, null); 785 }); 786 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 787 Assert.assertEquals(HELLO + JS_MESSAGE, data.mMessage); 788 } 789 790 private static final String TEST_PAGE_FOR_UNSUPPORTED_MESSAGES = "<!DOCTYPE html><html><body>" 791 + " <script>" 792 + " onmessage = function (e) {" 793 + " e.ports[0].postMessage(null);" 794 + " e.ports[0].postMessage(undefined);" 795 + " e.ports[0].postMessage(NaN);" 796 + " e.ports[0].postMessage(0);" 797 + " e.ports[0].postMessage(new Set());" 798 + " e.ports[0].postMessage({});" 799 + " e.ports[0].postMessage(['1','2','3']);" 800 + " e.ports[0].postMessage('" + JS_MESSAGE + "');" 801 + " }" 802 + " </script>" 803 + "</body></html>"; 804 805 // Make sure that postmessage can handle unsupported messages gracefully. 806 @Test 807 @SmallTest 808 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostUnsupportedWebMessageToApp()809 public void testPostUnsupportedWebMessageToApp() throws Throwable { 810 loadPage(TEST_PAGE_FOR_UNSUPPORTED_MESSAGES); 811 final ChannelContainer channelContainer = new ChannelContainer(); 812 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 813 MessagePort[] channel = mAwContents.createMessageChannel(); 814 channel[0].setMessageCallback( 815 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 816 mAwContents.postMessageToMainFrame( 817 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 818 }); 819 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 820 Assert.assertEquals(JS_MESSAGE, data.mMessage); 821 // Assert that onMessage is called only once. 822 Assert.assertTrue(channelContainer.isQueueEmpty()); 823 } 824 825 private static final String TEST_TRANSFER_EMPTY_PORTS = "<!DOCTYPE html><html><body>" 826 + " <script>" 827 + " onmessage = function (e) {" 828 + " e.ports[0].postMessage('1', undefined);" 829 + " e.ports[0].postMessage('2', []);" 830 + " }" 831 + " </script>" 832 + "</body></html>"; 833 834 // Make sure that postmessage can handle unsupported messages gracefully. 835 @Test 836 @SmallTest 837 @Feature({"AndroidWebView", "Android-PostMessage"}) testTransferEmptyPortsArray()838 public void testTransferEmptyPortsArray() throws Throwable { 839 loadPage(TEST_TRANSFER_EMPTY_PORTS); 840 final ChannelContainer channelContainer = new ChannelContainer(); 841 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 842 MessagePort[] channel = mAwContents.createMessageChannel(); 843 channel[0].setMessageCallback( 844 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 845 mAwContents.postMessageToMainFrame( 846 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 847 }); 848 ChannelContainer.Data data1 = channelContainer.waitForMessageCallback(); 849 Assert.assertEquals("1", data1.mMessage); 850 ChannelContainer.Data data2 = channelContainer.waitForMessageCallback(); 851 Assert.assertEquals("2", data2.mMessage); 852 } 853 854 // Make sure very large messages can be sent and received. 855 @Test 856 @SmallTest 857 @Feature({"AndroidWebView", "Android-PostMessage"}) testVeryLargeMessage()858 public void testVeryLargeMessage() throws Throwable { 859 mWebServer.setResponse(IFRAME_URL, ECHO_PAGE, null); 860 mActivityTestRule.triggerPopup(mAwContents, mContentsClient, mWebServer, 861 MAIN_PAGE_FOR_POPUP_TEST, POPUP_PAGE_WITH_IFRAME, POPUP_URL, "createPopup()"); 862 mActivityTestRule.connectPendingPopup(mAwContents); 863 final ChannelContainer channelContainer = new ChannelContainer(); 864 865 final StringBuilder longMessageBuilder = new StringBuilder(); 866 for (int i = 0; i < 100000; ++i) longMessageBuilder.append(HELLO); 867 final String longMessage = longMessageBuilder.toString(); 868 869 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 870 MessagePort[] channel = mAwContents.createMessageChannel(); 871 channel[0].setMessageCallback( 872 (message, sentPorts) -> channelContainer.notifyCalled(message), null); 873 mAwContents.postMessageToMainFrame( 874 WEBVIEW_MESSAGE, mWebServer.getBaseUrl(), new MessagePort[] {channel[1]}); 875 channel[0].postMessage(longMessage, null); 876 }); 877 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 878 Assert.assertEquals(longMessage + JS_MESSAGE, data.mMessage); 879 } 880 881 // Make sure messages are dispatched on the correct looper. 882 @Test 883 @SmallTest 884 @Feature({"AndroidWebView", "Android-PostMessage"}) testMessageOnCorrectLooper()885 public void testMessageOnCorrectLooper() throws Throwable { 886 final ChannelContainer channelContainer1 = new ChannelContainer(); 887 final ChannelContainer channelContainer2 = new ChannelContainer(); 888 final HandlerThread thread = new HandlerThread("test-thread"); 889 thread.start(); 890 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 891 MessagePort[] channel = mAwContents.createMessageChannel(); 892 channel[0].setMessageCallback( 893 (message, sentPorts) -> channelContainer1.notifyCalled(message), null); 894 channel[1].setMessageCallback((message, sentPorts) 895 -> channelContainer2.notifyCalled(message), 896 new Handler(thread.getLooper())); 897 channel[0].postMessage("foo", null); 898 channel[1].postMessage("bar", null); 899 }); 900 ChannelContainer.Data data1 = channelContainer1.waitForMessageCallback(); 901 ChannelContainer.Data data2 = channelContainer2.waitForMessageCallback(); 902 Assert.assertEquals("bar", data1.mMessage); 903 Assert.assertEquals(Looper.getMainLooper(), data1.mLastLooper); 904 Assert.assertEquals("foo", data2.mMessage); 905 Assert.assertEquals(thread.getLooper(), data2.mLastLooper); 906 } 907 908 // Make sure it is possible to change the message handler. 909 @Test 910 @SmallTest 911 @Feature({"AndroidWebView", "Android-PostMessage"}) testChangeMessageHandler()912 public void testChangeMessageHandler() throws Throwable { 913 final ChannelContainer channelContainer = new ChannelContainer(); 914 final HandlerThread thread = new HandlerThread("test-thread"); 915 thread.start(); 916 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 917 MessagePort[] channel = mAwContents.createMessageChannel(); 918 channelContainer.set(channel); 919 channel[0].setMessageCallback((message, sentPorts) 920 -> channelContainer.notifyCalled(message), 921 new Handler(thread.getLooper())); 922 channel[1].postMessage("foo", null); 923 }); 924 ChannelContainer.Data data = channelContainer.waitForMessageCallback(); 925 Assert.assertEquals("foo", data.mMessage); 926 Assert.assertEquals(thread.getLooper(), data.mLastLooper); 927 final ChannelContainer channelContainer2 = new ChannelContainer(); 928 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { 929 MessagePort[] channel = channelContainer.get(); 930 channel[0].setMessageCallback( 931 (message, sentPorts) -> channelContainer2.notifyCalled(message), null); 932 channel[1].postMessage("bar", null); 933 }); 934 ChannelContainer.Data data2 = channelContainer2.waitForMessageCallback(); 935 Assert.assertEquals("bar", data2.mMessage); 936 Assert.assertEquals(Looper.getMainLooper(), data2.mLastLooper); 937 } 938 939 // Regression test for crbug.com/973901 940 @Test 941 @SmallTest 942 @Feature({"AndroidWebView", "Android-PostMessage"}) testPostMessageBeforePageLoadWontBlockNavigation()943 public void testPostMessageBeforePageLoadWontBlockNavigation() throws Throwable { 944 final String baseUrl = mWebServer.getBaseUrl(); 945 946 // postMessage before page load. 947 TestThreadUtils.runOnUiThreadBlocking( 948 () -> mAwContents.postMessageToMainFrame("1", baseUrl, null)); 949 950 // loadPage shouldn't timeout. 951 loadPage(TEST_PAGE); 952 953 // Verify that after the page gets load, postMessage still works. 954 TestThreadUtils.runOnUiThreadBlocking( 955 () -> mAwContents.postMessageToMainFrame(WEBVIEW_MESSAGE, baseUrl, null)); 956 957 MessageObject.Data data = mMessageObject.waitForMessage(); 958 Assert.assertEquals(WEBVIEW_MESSAGE, data.mMessage); 959 Assert.assertEquals(SOURCE_ORIGIN, data.mOrigin); 960 } 961 } 962