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