[go: nahoru, domu]

blob: 852338100cc43004e4dcf979bfe0360a080b6e01 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.android_webview.test;
import static org.chromium.base.test.util.ScalableTimeout.scaleTimeout;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.webkit.JavascriptInterface;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import org.hamcrest.Matchers;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.UseParametersRunnerFactory;
import org.chromium.android_webview.AwContents;
import org.chromium.android_webview.test.util.CommonResources;
import org.chromium.base.ThreadUtils;
import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.CallbackHelper;
import org.chromium.base.test.util.Criteria;
import org.chromium.base.test.util.CriteriaHelper;
import org.chromium.base.test.util.Feature;
import org.chromium.content_public.browser.MessagePayload;
import org.chromium.content_public.browser.MessagePort;
import org.chromium.content_public.browser.test.util.TestCallbackHelperContainer.OnPageFinishedHelper;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.net.test.util.TestWebServer;
import java.io.UnsupportedEncodingException;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
/** The tests for content postMessage API. */
@Batch(Batch.PER_CLASS)
@RunWith(Parameterized.class)
@UseParametersRunnerFactory(AwJUnit4ClassRunnerWithParameters.Factory.class)
public class PostMessageTest extends AwParameterizedTest {
@Rule public AwActivityTestRule mActivityTestRule;
private static final String SOURCE_ORIGIN = "";
// Timeout to failure, in milliseconds
private static final long TIMEOUT = scaleTimeout(5000);
// Inject to the page to verify received messages.
private static class MessageObject {
private LinkedBlockingQueue<Data> mQueue = new LinkedBlockingQueue<>();
public static class Data {
public String mMessage;
public String mOrigin;
public int[] mPorts;
public Data(String message, String origin, int[] ports) {
mMessage = message;
mOrigin = origin;
mPorts = ports;
}
}
@JavascriptInterface
public void setMessageParams(String message, String origin, int[] ports) {
mQueue.add(new Data(message, origin, ports));
}
public Data waitForMessage() throws Exception {
return AwActivityTestRule.waitForNextQueueElement(mQueue);
}
}
private static class ChannelContainer {
private MessagePort[] mChannel;
private LinkedBlockingQueue<Data> mQueue = new LinkedBlockingQueue<>();
public static class Data {
public MessagePayload mMessagePayload;
public Looper mLastLooper;
public Data(MessagePayload messagePayload, Looper looper) {
mMessagePayload = messagePayload;
mLastLooper = looper;
}
public String getStringValue() {
return mMessagePayload.getAsString();
}
public byte[] getArrayBuffer() {
return mMessagePayload.getAsArrayBuffer();
}
}
public void set(MessagePort[] channel) {
mChannel = channel;
}
public MessagePort[] get() {
return mChannel;
}
public void notifyCalled(MessagePayload messagePayload) {
try {
mQueue.add(new Data(messagePayload, Looper.myLooper()));
} catch (IllegalStateException e) {
// We expect this add operation will always succeed since the default capacity of
// the queue is Integer.MAX_VALUE.
}
}
public Data waitForMessageCallback() throws Exception {
return AwActivityTestRule.waitForNextQueueElement(mQueue);
}
public boolean isQueueEmpty() {
return mQueue.isEmpty();
}
}
private MessageObject mMessageObject;
private TestAwContentsClient mContentsClient;
private AwTestContainerView mTestContainerView;
private AwContents mAwContents;
private TestWebServer mWebServer;
public PostMessageTest(AwSettingsMutation param) {
this.mActivityTestRule = new AwActivityTestRule(param.getMutation());
}
@Before
public void setUp() throws Exception {
mMessageObject = new MessageObject();
mContentsClient = new TestAwContentsClient();
mTestContainerView = mActivityTestRule.createAwTestContainerViewOnMainSync(mContentsClient);
mAwContents = mTestContainerView.getAwContents();
AwActivityTestRule.enableJavaScriptOnUiThread(mAwContents);
try {
AwActivityTestRule.addJavascriptInterfaceOnUiThread(
mAwContents, mMessageObject, "messageObject");
} catch (Throwable t) {
throw new RuntimeException(t);
}
mWebServer = TestWebServer.start();
}
@After
public void tearDown() {
mWebServer.shutdown();
}
private static final String WEBVIEW_MESSAGE = "from_webview";
private static final String JS_MESSAGE = "from_js";
private static final String TEST_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " messageObject.setMessageParams(e.data, e.origin, e.ports);"
+ " if (e.ports != null && e.ports.length > 0) {"
+ " e.ports[0].postMessage(\""
+ JS_MESSAGE
+ "\");"
+ " }"
+ " }"
+ " </script>"
+ "</body></html>";
// Concats all the data fields of the received messages and makes it
// available as page title.
private static final String TITLE_FROM_POSTMESSAGE_TO_FRAME =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received = '';"
+ " (e) {"
+ " received += e.data;"
+ " document.title = received;"
+ " }"
+ " </script>"
+ "</body></html>";
// Concats all the data fields of the received messages and makes it
// available as page title.
private static final String TITLE_FROM_POSTMESSAGE_TO_FRAME_ARRAYBUFFER =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received = '';"
+ " (e) {"
+ " const view = new Int8Array(e.data);"
+ " received += String.fromCharCode.apply(null, view);"
+ " document.title = received;"
+ " }"
+ " </script>"
+ "</body></html>";
// Concats all the data fields of the received messages to the transferred channel
// and makes it available as page title.
private static final String TITLE_FROM_POSTMESSAGE_TO_CHANNEL =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received = '';"
+ " (e) {"
+ " var myport = e.ports[0];"
+ " myport. (f) {"
+ " received += f.data;"
+ " document.title = received;"
+ " }"
+ " }"
+ " </script>"
+ "</body></html>";
// Concats all the data fields of the received messages to the transferred channel
// and makes it available as page title.
private static final String TITLE_FROM_POSTMESSAGE_TO_CHANNEL_ARRAYBUFFER =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " var myport = e.ports[0];"
+ " myport. (f) {"
+ " const view = new Int8Array(f.data);"
+ " document.title = String.fromCharCode.apply(null, view);"
+ " }"
+ " }"
+ " </script>"
+ "</body></html>";
// Call on non-UI thread.
private void expectTitle(String title) {
CriteriaHelper.pollUiThread(
() -> Criteria.checkThat(mAwContents.getTitle(), Matchers.is(title)));
}
private void loadPage(String page) throws Throwable {
final String url =
mWebServer.setResponse(
"/test.html", page, CommonResources.getTextHtmlHeaders(true));
OnPageFinishedHelper onPageFinishedHelper = mContentsClient.getOnPageFinishedHelper();
int currentCallCount = onPageFinishedHelper.getCallCount();
mActivityTestRule.loadUrlSync(mAwContents, mContentsClient.getOnPageFinishedHelper(), url);
onPageFinishedHelper.waitForCallback(currentCallCount);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToMainFrame() throws Throwable {
verifyPostMessageToMainFrame(mWebServer.getBaseUrl());
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToMainFrameUsingWildcard() throws Throwable {
verifyPostMessageToMainFrame("*");
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToMainFrameUsingEmptyStringAsWildcard() throws Throwable {
verifyPostMessageToMainFrame("");
}
private void verifyPostMessageToMainFrame(final String targetOrigin) throws Throwable {
loadPage(TEST_PAGE);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() ->
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE), targetOrigin, null));
MessageObject.Data data = mMessageObject.waitForMessage();
Assert.assertEquals(WEBVIEW_MESSAGE, data.mMessage);
Assert.assertEquals(SOURCE_ORIGIN, data.mOrigin);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostArrayBuffer() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_FRAME_ARRAYBUFFER);
final String testString = "TestString";
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
try {
mAwContents.postMessageToMainFrame(
new MessagePayload(testString.getBytes("UTF-8")),
mWebServer.getBaseUrl(),
null);
} catch (UnsupportedEncodingException e) {
Assert.fail();
}
});
expectTitle(testString);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostArrayBufferOnMessagePort() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL_ARRAYBUFFER);
final String testString = "TestString";
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
try {
channel[0].postMessage(
new MessagePayload(testString.getBytes("UTF-8")), null);
} catch (UnsupportedEncodingException e) {
}
channel[0].close();
});
expectTitle(testString);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferringSamePortTwiceViaPostMessageToMainFrameNotAllowed()
throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
// Retransfer the port. This should fail with an exception.
try {
mAwContents.postMessageToMainFrame(
new MessagePayload("2"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// There are two cases that put a port in a started state.
// 1. posting a message
// 2. setting an event handler.
// A started port cannot return to "non-started" state. The four tests below verifies
// these conditions for both conditions, using message ports and message channels.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[1].postMessage(new MessagePayload("1"), null);
try {
mAwContents.postMessageToMainFrame(
new MessagePayload("2"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// see documentation in testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingPostMessageToMainFrame2() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
// set a web event handler, this puts the port in a started state.
channel[1].setMessageCallback((message, sentPorts) -> {}, null);
try {
mAwContents.postMessageToMainFrame(
new MessagePayload("2"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// see documentation in testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingMessageChannel1() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel1 = mAwContents.createMessageChannel();
channel1[1].postMessage(new MessagePayload("1"), null);
MessagePort[] channel2 = mAwContents.createMessageChannel();
try {
channel2[0].postMessage(
new MessagePayload("2"), new MessagePort[] {channel1[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// see documentation in testStartedPortCannotBeTransferredUsingPostMessageToMainFrame1
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testStartedPortCannotBeTransferredUsingMessageChannel2() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel1 = mAwContents.createMessageChannel();
// set a web event handler, this puts the port in a started state.
channel1[1].setMessageCallback((message, sentPorts) -> {}, null);
MessagePort[] channel2 = mAwContents.createMessageChannel();
try {
channel2[0].postMessage(
new MessagePayload("1"), new MessagePort[] {channel1[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// channel[0] and channel[1] are entangled ports, establishing a channel. Verify
// it is not allowed to transfer channel[0] on channel[0].postMessage.
// TODO(sgurun) Note that the related case of posting channel[1] via
// channel[0].postMessage does not throw a JS exception at present. We do not throw
// an exception in this case either since the information of entangled port is not
// available at the source port. We need a new mechanism to implement to prevent
// this case.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferringSourcePortViaMessageChannelNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
try {
channel[0].postMessage(
new MessagePayload("1"), new MessagePort[] {channel[0]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// Verify a closed port cannot be transferred to a frame.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testSendClosedPortToFrameNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[1].close();
try {
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// Verify a closed port cannot be transferred to a port.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testSendClosedPortToPortNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel1 = mAwContents.createMessageChannel();
MessagePort[] channel2 = mAwContents.createMessageChannel();
channel2[1].close();
try {
channel1[0].postMessage(
new MessagePayload("1"), new MessagePort[] {channel2[1]});
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// Verify messages cannot be posted to closed ports.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToClosedPortNotAllowed() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].close();
try {
channel[0].postMessage(new MessagePayload("1"), null);
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// Verify messages posted before closing a port is received at the destination port.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessagesPostedBeforeClosingPortAreTransferred() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload("2"), null);
channel[0].postMessage(new MessagePayload("3"), null);
channel[0].close();
});
expectTitle("23");
}
// Verify a transferred port using postMessageToMainFrame cannot be closed.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testClosingTransferredPortToFrameThrowsAnException() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
try {
channel[1].close();
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// Verify a transferred port using postMessageToMainFrame cannot be closed.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testClosingTransferredPortToChannelThrowsAnException() throws Throwable {
loadPage(TEST_PAGE);
final CountDownLatch latch = new CountDownLatch(1);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel1 = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel1[1]});
MessagePort[] channel2 = mAwContents.createMessageChannel();
channel1[0].postMessage(
new MessagePayload("2"), new MessagePort[] {channel2[0]});
try {
channel2[0].close();
} catch (IllegalStateException ex) {
latch.countDown();
return;
}
Assert.fail();
});
boolean ignore = latch.await(TIMEOUT, TimeUnit.MILLISECONDS);
}
// Create two message channels, and while they are in pending state, transfer the
// second one in the first one.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPendingPortCanBeTransferredInPendingPort() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_CHANNEL);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel1 = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel1[1]});
MessagePort[] channel2 = mAwContents.createMessageChannel();
channel1[0].postMessage(
new MessagePayload("2"), new MessagePort[] {channel2[0]});
});
expectTitle("2");
}
private static final String ECHO_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " var myPort = e.ports[0];"
+ " myPort. {"
+ " myPort.postMessage(e.data + \""
+ JS_MESSAGE
+ "\"); }"
+ " }"
+ " </script>"
+ "</body></html>";
private static final String ECHO_ARRAY_BUFFER_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " var myPort = e.ports[0];"
+ " myPort. {"
+ " myPort.postMessage(e.data, [e.data]); }"
+ " }"
+ " </script>"
+ "</body></html>";
private static final String ECHO_NON_TRANFERABLE_ARRAY_BUFFER_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " var myPort = e.ports[0];"
+ " myPort. {"
+ " myPort.postMessage(e.data, [e.data]); }"
+ " }"
+ " </script>"
+ "</body></html>";
private static final String HELLO = "HELLO";
// Message channels are created on UI thread. Verify that a message port
// can be transferred to JS and full communication can happen on it. Do
// this by sending a message to JS and letting it echo the message with
// some text prepended to it.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelUsingInitializedPort() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(ECHO_PAGE);
final MessagePort[] channel =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload(HELLO), null);
});
// wait for the asynchronous response from JS
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(HELLO + JS_MESSAGE, data.getStringValue());
}
// Verify that a message port can be used immediately (even if it is in
// pending state) after creation. In particular make sure the message port can be
// transferred to JS and full communication can happen on it.
// Do this by sending a message to JS and let it echo'ing the message with
// some text prepended to it.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
@Test
public void testMessageChannelUsingPendingPort() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(ECHO_PAGE);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload(HELLO), null);
});
// Wait for the asynchronous response from JS.
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(HELLO + JS_MESSAGE, data.getStringValue());
}
// Verify that a message port can be used for message transfer when both
// ports are owned by same Webview.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelCommunicationWithinWebView() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(ECHO_PAGE);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[1].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
channel[0].postMessage(new MessagePayload(HELLO), null);
});
// Wait for the asynchronous response from JS.
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(HELLO, data.getStringValue());
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelSendAndReceiveArrayBuffer() throws Throwable {
final byte[] bytes = HELLO.getBytes("UTF-8");
verifyEchoArrayBuffer(ECHO_ARRAY_BUFFER_PAGE, bytes);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelSendAndReceiveLargeArrayBuffer() throws Throwable {
final byte[] bytes = new byte[1000 * 1000]; // 1MB
new Random(42).nextBytes(bytes);
verifyEchoArrayBuffer(ECHO_ARRAY_BUFFER_PAGE, bytes);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelSendAndReceiveNonTransferableArrayBuffer() throws Throwable {
final byte[] bytes = HELLO.getBytes("UTF-8");
verifyEchoArrayBuffer(ECHO_NON_TRANFERABLE_ARRAY_BUFFER_PAGE, bytes);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageChannelSendAndReceiveLargeNonTransferableArrayBuffer() throws Throwable {
final byte[] bytes = new byte[1000 * 1000]; // 1MB
new Random(42).nextBytes(bytes);
verifyEchoArrayBuffer(ECHO_NON_TRANFERABLE_ARRAY_BUFFER_PAGE, bytes);
}
private void verifyEchoArrayBuffer(final String page, final byte[] bytes) throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(page);
final MessagePort[] channel =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload(bytes), null);
});
// wait for the asynchronous response from JS
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertArrayEquals(bytes, data.getArrayBuffer());
}
// Post a message with a pending port to a frame and then post a bunch of messages
// after that. Make sure that they are not ordered at the receiver side.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToMainFrameNotReordersMessages() throws Throwable {
loadPage(TITLE_FROM_POSTMESSAGE_TO_FRAME);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
mAwContents.postMessageToMainFrame(
new MessagePayload("2"), mWebServer.getBaseUrl(), null);
mAwContents.postMessageToMainFrame(
new MessagePayload("3"), mWebServer.getBaseUrl(), null);
});
expectTitle("123");
}
// Generate an arraybuffer with a given size, and fill with ordered number, 0-255.
// Then pass it back over MessagePort.
private static final String GENERATE_ARRAY_BUFFER_FROM_JS_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " var myPort = e.ports[0];"
+ " myPort. {"
+ " let length = parseInt(e.data, 10);"
+ " var arrayBuffer = new ArrayBuffer(length);"
+ " const view = new Uint8Array(arrayBuffer);"
+ " for (var i = 0; i < length; ++i) {"
+ " view[i] = i;"
+ " }"
+ " myPort.postMessage(arrayBuffer, [arrayBuffer]);"
+ " };"
+ " };"
+ " </script>";
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testReceiveArrayBufferFromJsOverMessagePort() throws Throwable {
final int bufferLength = 5000;
final byte[] expectedBytes = new byte[bufferLength];
for (int i = 0; i < bufferLength; ++i) {
// Cast to byte implicitly % 256.
expectedBytes[i] = (byte) i;
}
final ChannelContainer channelContainer = new ChannelContainer();
loadPage(GENERATE_ARRAY_BUFFER_FROM_JS_PAGE);
final MessagePort[] channel =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(
new MessagePayload(String.valueOf(bufferLength)), null);
});
// wait for the asynchronous response from JS
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
final byte[] bytes = data.getArrayBuffer();
Assert.assertEquals(bufferLength, bytes.length);
Assert.assertArrayEquals(expectedBytes, bytes);
}
private static final String RECEIVE_JS_MESSAGE_CHANNEL_PAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var received ='';"
+ " var mc = new MessageChannel();"
+ " mc.port1. (e) {"
+ " received += e.data;"
+ " document.title = received;"
+ " if (e.data == '2') { mc.port1.postMessage('3'); }"
+ " };"
+ " (e) {"
+ " var myPort = e.ports[0];"
+ " myPort.postMessage('from window', [mc.port2]);"
+ " }"
+ " </script>"
+ "</body></html>";
// Test webview can use a message port received from JS for full duplex communication.
// Test steps:
// 1. Java creates a message channel, and send one port to JS
// 2. JS creates a new message channel and sends one port to Java using the channel in 1
// 3. Java sends a message using the new channel in 2.
// 4. Js responds to this message using the channel in 2.
// 5. Java responds to message in 4 using the channel in 2.
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
@Test
public void testCanUseReceivedAwMessagePortFromJS() throws Throwable {
loadPage(RECEIVE_JS_MESSAGE_CHANNEL_PAGE);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload("1"),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].setMessageCallback(
(message, p) -> {
p[0].setMessageCallback(
(message1, q) -> {
Assert.assertEquals(
"3", message1.getAsString());
p[0].postMessage(new MessagePayload("4"), null);
},
null);
p[0].postMessage(new MessagePayload("2"), null);
},
null);
});
expectTitle("24");
}
private static final String WORKER_MESSAGE = "from_worker";
// Listen for messages. Pass port 1 to worker and use port 2 to receive messages from
// from worker.
private static final String TEST_PAGE_FOR_PORT_TRANSFER =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var worker = new Worker(\"worker.js\");"
+ " (e) {"
+ " if (e.data == \""
+ WEBVIEW_MESSAGE
+ "\") {"
+ " worker.postMessage(\"worker_port\", [e.ports[0]]);"
+ " var messageChannelPort = e.ports[1];"
+ " messageChannelPort.>
+ " }"
+ " };"
+ " function receiveWorkerMessage(e) {"
+ " if (e.data == \""
+ WORKER_MESSAGE
+ "\") {"
+ " messageObject.setMessageParams(e.data, e.origin, e.ports);"
+ " }"
+ " };"
+ " </script>"
+ "</body></html>";
private static final String WORKER_SCRIPT =
" {"
+ " if (e.data == \"worker_port\") {"
+ " var toWindow = e.ports[0];"
+ " toWindow.postMessage(\""
+ WORKER_MESSAGE
+ "\");"
+ " toWindow.start();"
+ " }"
+ "}";
// Test if message ports created at the native side can be transferred
// to JS side, to establish a communication channel between a worker and a frame.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferPortsToWorker() throws Throwable {
mWebServer.setResponse(
"/worker.js", WORKER_SCRIPT, CommonResources.getTextJavascriptHeaders(true));
loadPage(TEST_PAGE_FOR_PORT_TRANSFER);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[0], channel[1]});
});
MessageObject.Data data = mMessageObject.waitForMessage();
Assert.assertEquals(WORKER_MESSAGE, data.mMessage);
}
private static final String POPUP_MESSAGE = "from_popup";
private static final String POPUP_URL = "/popup.html";
private static final String IFRAME_URL = "/iframe.html";
private static final String MAIN_PAGE_FOR_POPUP_TEST =
"<!DOCTYPE html><html>"
+ "<head>"
+ " <script>"
+ " function createPopup() {"
+ " var popupWindow = window.open('"
+ POPUP_URL
+ "');"
+ " {"
+ " popupWindow.postMessage(e.data, '*', e.ports);"
+ " };"
+ " }"
+ " </script>"
+ "</head>"
+ "</html>";
// Sends message and ports to the iframe.
private static final String POPUP_PAGE_WITH_IFRAME =
"<!DOCTYPE html><html>"
+ "<script>"
+ " {"
+ " var iframe = document.getElementsByTagName('iframe')[0];"
+ " iframe.contentWindow.postMessage('"
+ POPUP_MESSAGE
+ "', '*', e.ports);"
+ " };"
+ "</script>"
+ "<body><iframe src='"
+ IFRAME_URL
+ "'></iframe></body>"
+ "</html>";
// Test if WebView can post a message from/to a popup window owning a message port.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToPopup() throws Throwable {
mActivityTestRule.triggerPopup(
mAwContents,
mContentsClient,
mWebServer,
MAIN_PAGE_FOR_POPUP_TEST,
ECHO_PAGE,
POPUP_URL,
"createPopup()");
mActivityTestRule.connectPendingPopup(mAwContents);
final ChannelContainer channelContainer = new ChannelContainer();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload(HELLO), null);
});
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(HELLO + JS_MESSAGE, data.getStringValue());
}
// Test if WebView can post a message from/to an iframe in a popup window.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageToIframeInsidePopup() throws Throwable {
mWebServer.setResponse(IFRAME_URL, ECHO_PAGE, null);
mActivityTestRule.triggerPopup(
mAwContents,
mContentsClient,
mWebServer,
MAIN_PAGE_FOR_POPUP_TEST,
POPUP_PAGE_WITH_IFRAME,
POPUP_URL,
"createPopup()");
mActivityTestRule.connectPendingPopup(mAwContents);
final ChannelContainer channelContainer = new ChannelContainer();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload(HELLO), null);
});
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(HELLO + JS_MESSAGE, data.getStringValue());
}
private static final String TEST_PAGE_FOR_UNSUPPORTED_MESSAGES =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " e.ports[0].postMessage(null);"
+ " e.ports[0].postMessage(undefined);"
+ " e.ports[0].postMessage(NaN);"
+ " e.ports[0].postMessage(0);"
+ " e.ports[0].postMessage(new Set());"
+ " e.ports[0].postMessage({});"
+ " e.ports[0].postMessage(['1','2','3']);"
+ " e.ports[0].postMessage('"
+ JS_MESSAGE
+ "');"
+ " }"
+ " </script>"
+ "</body></html>";
// Make sure that postmessage can handle unsupported messages gracefully.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostUnsupportedWebMessageToApp() throws Throwable {
loadPage(TEST_PAGE_FOR_UNSUPPORTED_MESSAGES);
final ChannelContainer channelContainer = new ChannelContainer();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
});
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(JS_MESSAGE, data.getStringValue());
// Assert that onMessage is called only once.
Assert.assertTrue(channelContainer.isQueueEmpty());
}
private static final String TEST_TRANSFER_EMPTY_PORTS =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " (e) {"
+ " e.ports[0].postMessage('1', undefined);"
+ " e.ports[0].postMessage('2', []);"
+ " }"
+ " </script>"
+ "</body></html>";
// Make sure that postmessage can handle unsupported messages gracefully.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferEmptyPortsArray() throws Throwable {
loadPage(TEST_TRANSFER_EMPTY_PORTS);
final ChannelContainer channelContainer = new ChannelContainer();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
});
ChannelContainer.Data data1 = channelContainer.waitForMessageCallback();
Assert.assertEquals("1", data1.getStringValue());
ChannelContainer.Data data2 = channelContainer.waitForMessageCallback();
Assert.assertEquals("2", data2.getStringValue());
}
// Make sure very large messages can be sent and received.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testVeryLargeMessage() throws Throwable {
mWebServer.setResponse(IFRAME_URL, ECHO_PAGE, null);
mActivityTestRule.triggerPopup(
mAwContents,
mContentsClient,
mWebServer,
MAIN_PAGE_FOR_POPUP_TEST,
POPUP_PAGE_WITH_IFRAME,
POPUP_URL,
"createPopup()");
mActivityTestRule.connectPendingPopup(mAwContents);
final ChannelContainer channelContainer = new ChannelContainer();
final StringBuilder longMessageBuilder = new StringBuilder();
for (int i = 0; i < 100000; ++i) longMessageBuilder.append(HELLO);
final String longMessage = longMessageBuilder.toString();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
null);
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE),
mWebServer.getBaseUrl(),
new MessagePort[] {channel[1]});
channel[0].postMessage(new MessagePayload(longMessage), null);
});
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals(longMessage + JS_MESSAGE, data.getStringValue());
}
// Make sure messages are dispatched on the correct looper.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessageOnCorrectLooper() throws Throwable {
final ChannelContainer channelContainer1 = new ChannelContainer();
final ChannelContainer channelContainer2 = new ChannelContainer();
final HandlerThread thread = new HandlerThread("test-thread");
thread.start();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer1.notifyCalled(message),
null);
channel[1].setMessageCallback(
(message, sentPorts) -> channelContainer2.notifyCalled(message),
new Handler(thread.getLooper()));
channel[0].postMessage(new MessagePayload("foo"), null);
channel[1].postMessage(new MessagePayload("bar"), null);
});
ChannelContainer.Data data1 = channelContainer1.waitForMessageCallback();
ChannelContainer.Data data2 = channelContainer2.waitForMessageCallback();
Assert.assertEquals("bar", data1.getStringValue());
Assert.assertEquals(Looper.getMainLooper(), data1.mLastLooper);
Assert.assertEquals("foo", data2.getStringValue());
Assert.assertEquals(thread.getLooper(), data2.mLastLooper);
}
// Make sure it is possible to change the message handler.
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testChangeMessageHandler() throws Throwable {
final ChannelContainer channelContainer = new ChannelContainer();
final HandlerThread thread = new HandlerThread("test-thread");
thread.start();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = mAwContents.createMessageChannel();
channelContainer.set(channel);
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer.notifyCalled(message),
new Handler(thread.getLooper()));
channel[1].postMessage(new MessagePayload("foo"), null);
});
ChannelContainer.Data data = channelContainer.waitForMessageCallback();
Assert.assertEquals("foo", data.getStringValue());
Assert.assertEquals(thread.getLooper(), data.mLastLooper);
final ChannelContainer channelContainer2 = new ChannelContainer();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] channel = channelContainer.get();
channel[0].setMessageCallback(
(message, sentPorts) -> channelContainer2.notifyCalled(message),
null);
channel[1].postMessage(new MessagePayload("bar"), null);
});
ChannelContainer.Data data2 = channelContainer2.waitForMessageCallback();
Assert.assertEquals("bar", data2.getStringValue());
Assert.assertEquals(Looper.getMainLooper(), data2.mLastLooper);
}
// Regression test for crbug.com/973901
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testPostMessageBeforePageLoadWontBlockNavigation() throws Throwable {
final String baseUrl = mWebServer.getBaseUrl();
// postMessage before page load.
TestThreadUtils.runOnUiThreadBlocking(
() -> mAwContents.postMessageToMainFrame(new MessagePayload("1"), baseUrl, null));
// loadPage shouldn't timeout.
loadPage(TEST_PAGE);
// Verify that after the page gets load, postMessage still works.
TestThreadUtils.runOnUiThreadBlocking(
() ->
mAwContents.postMessageToMainFrame(
new MessagePayload(WEBVIEW_MESSAGE), baseUrl, null));
MessageObject.Data data = mMessageObject.waitForMessage();
Assert.assertEquals(WEBVIEW_MESSAGE, data.mMessage);
Assert.assertEquals(SOURCE_ORIGIN, data.mOrigin);
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testMessagePortLifecycle() throws Throwable {
final String baseUrl = mWebServer.getBaseUrl();
loadPage(TEST_PAGE);
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
final MessagePort[] ports = mAwContents.createMessageChannel();
Assert.assertFalse(ports[0].isTransferred());
Assert.assertFalse(ports[0].isClosed());
Assert.assertFalse(ports[0].isStarted());
Assert.assertFalse(ports[1].isTransferred());
Assert.assertFalse(ports[1].isClosed());
Assert.assertFalse(ports[1].isStarted());
// Post port1 to main frame.
mAwContents.postMessageToMainFrame(
new MessagePayload("1"), baseUrl, new MessagePort[] {ports[1]});
Assert.assertTrue(ports[1].isTransferred());
Assert.assertFalse(ports[1].isClosed());
Assert.assertFalse(ports[1].isStarted());
// Close one port.
ports[0].close();
Assert.assertFalse(ports[0].isTransferred());
Assert.assertTrue(ports[0].isClosed());
Assert.assertFalse(ports[0].isStarted());
});
}
private static final String COUNT_PORT_FROM_MESSAGE =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var counter = 0;"
+ " var received = '';"
+ " (e) {"
+ " e.ports[0]. {"
+ " received += e.data;"
+ " counter += e.ports.length;"
+ " document.title = received + counter;"
+ " e.ports[0].postMessage(received + counter);"
+ " };"
+ " };"
+ " </script>"
+ "</body></html>";
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
// Previously postMessage can be called on any thread, but no tests or CTS tests checked.
public void testTransferPortOnAnotherThread() throws Throwable {
loadPage(COUNT_PORT_FROM_MESSAGE);
final ChannelContainer container = new ChannelContainer();
final MessagePort[] ports =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mAwContents.postMessageToMainFrame(
new MessagePayload(""), "*", new MessagePort[] {ports[1]});
});
final MessagePort[] ports2 =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
ports2[0].setMessageCallback(
(messagePayload, sentPorts) -> {
ThreadUtils.checkUiThread();
container.notifyCalled(messagePayload);
},
null);
ports[0].postMessage(new MessagePayload(HELLO), new MessagePort[] {ports2[1]});
expectTitle(HELLO + "1");
Assert.assertEquals(HELLO + "1", container.waitForMessageCallback().getStringValue());
ports[0].close();
ports2[0].close();
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferPortImmediateAfterPostMessageOnAnotherThread() throws Throwable {
loadPage(COUNT_PORT_FROM_MESSAGE);
final MessagePort[] ports =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mAwContents.postMessageToMainFrame(
new MessagePayload(""), "*", new MessagePort[] {ports[1]});
});
final CallbackHelper callbackHelper = new CallbackHelper();
final AtomicReference<IllegalStateException> exceptionRef = new AtomicReference<>();
PostTask.postTask(
TaskTraits.UI_DEFAULT,
() -> {
try {
callbackHelper.waitForCallback(0);
mAwContents.postMessageToMainFrame(
new MessagePayload(HELLO), "*", new MessagePort[] {ports[0]});
} catch (TimeoutException ignored) {
} catch (IllegalStateException e) {
exceptionRef.set(e);
callbackHelper.notifyCalled();
}
});
ports[0].postMessage(new MessagePayload(HELLO), null);
callbackHelper.notifyCalled();
callbackHelper.waitForCallback(1);
Assert.assertEquals("Port is already started", exceptionRef.get().getMessage());
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testCloseMessagePortOnAnotherThread() throws Throwable {
final MessagePort[] messagePorts = new MessagePort[1];
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
final MessagePort[] ports = mAwContents.createMessageChannel();
messagePorts[0] = ports[0];
// Move message port into |receiving| state.
messagePorts[0].setMessageCallback(
(messagePayload, sentPorts) -> {}, null);
});
// Close message channel on another thread, simulate the case where the "finalize" is called
// on finalizer thread.
messagePorts[0].close();
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testTransferPortInAnotherThreadRaceCondition() throws Throwable {
loadPage(COUNT_PORT_FROM_MESSAGE);
final MessagePort[] ports =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
TestThreadUtils.runOnUiThreadBlocking(
() -> {
mAwContents.postMessageToMainFrame(
new MessagePayload(""), "*", new MessagePort[] {ports[1]});
});
final MessagePort[] portsToTransfer =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
// Transfer the port in another thread.
ports[0].postMessage(new MessagePayload("test"), new MessagePort[] {portsToTransfer[0]});
// Check port2[0] is transferred right now.
Assert.assertTrue(portsToTransfer[0].isTransferred());
// Set callback on the just transferred port right now. It should fail.
try {
portsToTransfer[0].setMessageCallback((messagePayload, sentPorts) -> {}, null);
Assert.fail("Port transferred, should not able to listen on");
} catch (IllegalStateException e) {
// Ignored.
}
}
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
public void testSetReceiverAfterMessageReceived() throws Throwable {
loadPage(COUNT_PORT_FROM_MESSAGE);
final ChannelContainer container = new ChannelContainer();
final HandlerThread thread = new HandlerThread("test-thread");
thread.start();
final Handler handler = new Handler(thread.getLooper());
final MessagePort[] ports =
TestThreadUtils.runOnUiThreadBlocking(() -> mAwContents.createMessageChannel());
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
// Post message before set callback
ports[0].postMessage(new MessagePayload("msg1"), null);
});
ports[1].setMessageCallback(
(messagePayload, sentPorts) -> {
container.notifyCalled(messagePayload);
},
handler);
Assert.assertEquals("msg1", container.waitForMessageCallback().getStringValue());
}
private static final String COPY_PORT_MESSAGE_FROM_WINDOW =
"<!DOCTYPE html><html><body>"
+ " <script>"
+ " var port = null;"
+ " (e) {"
+ " if (e.ports[0]) port = e.ports[0];"
+ " else port.postMessage(e.data);"
+ " };"
+ " </script>"
+ "</body></html>";
@Test
@SmallTest
@Feature({"AndroidWebView", "Android-PostMessage"})
// Regression test of https://issuetracker.google.com/245837736
public void testMessageListenerAvailableAfterPortGarbageCollected() throws Throwable {
loadPage(COPY_PORT_MESSAGE_FROM_WINDOW);
final ChannelContainer container = new ChannelContainer();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
MessagePort[] ports = mAwContents.createMessageChannel();
ports[0].setMessageCallback(
(message, p) -> container.notifyCalled(message), null);
mAwContents.postMessageToMainFrame(
new MessagePayload("*"), "*", new MessagePort[] {ports[1]});
ports = null;
});
for (int i = 0; i < 100; ++i) {
final String message = HELLO + i;
Runtime.getRuntime().gc();
InstrumentationRegistry.getInstrumentation()
.runOnMainSync(
() -> {
// Trigger GC to make ports[0] being garbage collected. Note that
// despite that what JavaDoc says about invoking "gc()", both
// Dalvik and ART actually run the collector.
mAwContents.postMessageToMainFrame(
new MessagePayload(message), "*", null);
});
Assert.assertEquals(message, container.waitForMessageCallback().getStringValue());
}
}
}