diff --git a/playwright/src/main/java/com/microsoft/playwright/Android.java b/playwright/src/main/java/com/microsoft/playwright/Android.java
new file mode 100644
index 000000000..a36857afa
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/Android.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Playwright has experimental support for Android automation. This includes
+ * Chrome for Android and Android
+ * WebView.
+ *
+ *
+ * Requirements
+ *
+ * - Android device or AVD Emulator.
+ * - ADB
+ * daemon running and authenticated with your device.
+ * Typically running {@code adb devices} is all you need to do.
+ * - Chrome
+ * 87 or newer installed on the
+ * device
+ * - "Enable command line on non-rooted devices" enabled in
+ * {@code chrome://flags}.
+ *
+ *
+ *
+ * Known limitations
+ *
+ * - Raw USB operation is not yet supported, so you need ADB.
+ * - Device needs to be awake to produce screenshots. Enabling "Stay awake"
+ * developer mode will help.
+ * - We didn't run all the tests against the device, so not everything
+ * works.
+ *
+ * How to run
+ *
+ *
An example of the Android automation script would be:
+ *
{@code
+ * import com.microsoft.playwright.*;
+ * import java.util.regex.Pattern;
+ * import java.util.List;
+ *
+ * public class Example {
+ * public static void main(String[] args) {
+ * try (Playwright playwright = Playwright.create()) {
+ * Android android = playwright.android();
+ * List devices = android.devices();
+ * AndroidDevice device = devices.get(0);
+ * System.out.println("Model: " + device.model());
+ * System.out.println("Serial: " + device.serial());
+ * // Take screenshot of the whole device.
+ * ScreenshotOptions screenshotOptions = new ScreenshotOptions();
+ * screenshotOptions.setPath("device.png");
+ * device.screenshot(screenshotOptions);
+ *
+ * // --------------------- WebView -----------------------
+ *
+ * // Launch an application with WebView.
+ * device.shell("am force-stop org.chromium.webview_shell");
+ * device.shell("am start -n org.chromium.webview_shell/.WebViewBrowserActivity");
+ * // Get the WebView.
+ * WebViewSelector selector = new WebViewSelector();
+ * selector.setPkg("org.chromium.webview_shell");
+ * AndroidWebView webview = device.webView(selector);
+ * // Fill the input box.
+ * AndroidSelector inputSelector = new AndroidSelector();
+ * inputSelector.setRes("org.chromium.webview_shell:id/url_field");
+ * device.fill(inputSelector, 'github.com/microsoft/playwright');
+ * device.press(inputSelector, AndroidKey.ENTER);
+ *
+ * // Work with WebView's page as usual.
+ * Page page = webview.page();
+ * WaitForNavigationOptions waitForNavigationOptions = new WaitForNavigationOptions();
+ * waitForNavigationOptions.setUrl(Pattern.compile(".*github.com/microsoft/playwright.*"));
+ * page.waitForNavigation(waitForNavigationOptions);
+ * System.out.println(page.title());
+ *
+ * // --------------------- Browser -----------------------
+ *
+ * // Launch Chrome browser.
+ * device.shell("am force-stop com.android.chrome");
+ * BrowserContext context = device.launchBrowser();
+ *
+ * // Use BrowserContext as usual.
+ * Page page = context.newPage();
+ * page.navigate("https://webkit.org/");
+ * System.out.println(page.evaluate("() => window.location.href)");
+ *
+ * context.close();
+ *
+ * // Close the device.
+ * device.close();
+ * }
+ * }
+ * }
+ * }
+ */
+public interface Android {
+ /**
+ * Options for {@link Android#connect(String, ConnectOptions)}.
+ */
+ class ConnectOptions {
+ /**
+ * Maximum time in milliseconds to wait for the connection to be established. Defaults to `30000` (30 seconds). Pass
+ * `0` to disable timeout.
+ */
+ public Double timeout;
+ /**
+ * Additional HTTP headers to send with the WebSocket connection.
+ */
+ public Map headers;
+ /**
+ * Slows down operations by the specified amount of milliseconds.
+ */
+ public Double slowMo;
+
+ /**
+ * Maximum time to wait for in milliseconds. Defaults to {@code 30000} (30 seconds). Pass {@code 0} to disable timeout. The
+ * default value can be changed by using the {@link com.microsoft.playwright.Android#setDefaultTimeout
+ * BrowserContext.setDefaultTimeout()}.
+ */
+ public ConnectOptions setTimeout(double timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+ /**
+ * Additional HTTP headers to be sent with web socket connect request. Optional.
+ */
+ public ConnectOptions setHeaders(Map headers) {
+ this.headers = headers;
+ return this;
+ }
+ /**
+ * Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going
+ * on. Defaults to `0`.
+ */
+ public ConnectOptions setSlowMo(double slowMo) {
+ this.slowMo = slowMo;
+ return this;
+ }
+ }
+ /**
+ * Options for {@link Android#devices(DevicesOptions)}.
+ */
+ class DevicesOptions {
+ /**
+ * Optional port to establish ADB server connection. Default to `5037`.
+ */
+ public Integer port;
+ /**
+ * Optional host to establish ADB server connection. Default to `127.0.0.1`.
+ */
+ public String host;
+ /**
+ * Prevents automatic playwright driver installation on attach. Assumes that the drivers have been installed already.
+ */
+ public Boolean omitDriverInstall;
+
+ /**
+ * Optional port to connect to ADB.
+ */
+ public DevicesOptions setPort(int port) {
+ this.port = port;
+ return this;
+ }
+ /**
+ * t
+ * Optional host to connect to ADB.
+ */
+ public DevicesOptions setHost(String host) {
+ this.host = host;
+ return this;
+ }
+ /**
+ * Prevents automatic playwright driver installation on attach. Assumes that the
+ * drivers have been installed already.
+ */
+ public DevicesOptions setOmitDriverInstall(boolean omitDriverInstall) {
+ this.omitDriverInstall = omitDriverInstall;
+ return this;
+ }
+ }
+
+ /**
+ * This methods attaches Playwright to an existing Android device.
+ *
+ * @param wsEndpoint A browser websocket endpoint to connect to.
+ */
+ default AndroidDevice connect(String wsEndpoint) {
+ return connect(wsEndpoint, null);
+ }
+ /**
+ * Connects to an Android device via WebSocket endpoint with custom options.
+ *
+ * @param wsEndpoint A browser websocket endpoint to connect to.
+ * @param options Connection options
+ */
+ AndroidDevice connect(String wsEndpoint, ConnectOptions options);
+ /**
+ * Returns a list of detected Android devices.
+ */
+ default List devices() {
+ return devices(null);
+ }
+ /**
+ * Returns a list of detected Android devices.
+ *
+ * @param options Connection options with optional port
+ */
+ List devices(DevicesOptions options);
+ /**
+ * This setting will change the default maximum time for all the methods accepting {@code timeout} option.
+ *
+ * @param timeout Maximum time in milliseconds. Pass {@code 0} to disable timeout.
+ */
+ void setDefaultTimeout(double timeout);
+
+}
diff --git a/playwright/src/main/java/com/microsoft/playwright/AndroidDevice.java b/playwright/src/main/java/com/microsoft/playwright/AndroidDevice.java
new file mode 100644
index 000000000..8a7242421
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/AndroidDevice.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright;
+
+import java.util.function.Consumer;
+
+public interface AndroidDevice extends AutoCloseable {
+ /**
+ * Emitted when the device connection gets closed.
+ */
+ void onClose(Consumer handler);
+ /**
+ * Removes handler that was previously added with {@link #onClose onClose(Consumer)}.
+ */
+ void offClose(Consumer handler);
+ /**
+ * Closes the device connection.
+ */
+ void close();
+ /**
+ * Device model.
+ */
+ String model();
+ /**
+ * Device serial number.
+ */
+ String serial();
+}
diff --git a/playwright/src/main/java/com/microsoft/playwright/Playwright.java b/playwright/src/main/java/com/microsoft/playwright/Playwright.java
index 2e69e5f5f..7a610599a 100644
--- a/playwright/src/main/java/com/microsoft/playwright/Playwright.java
+++ b/playwright/src/main/java/com/microsoft/playwright/Playwright.java
@@ -87,6 +87,11 @@ public CreateOptions setEnv(Map env) {
* @since v1.8
*/
BrowserType webkit();
+ /**
+ * This object can be used to launch or connect to Android, returning instances of {@code Android}.
+ *
+ */
+ Android android();
/**
* Terminates this instance of Playwright, will also close all created browsers if they are still running.
*
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/AndroidDeviceImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/AndroidDeviceImpl.java
new file mode 100644
index 000000000..5f05b10ee
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/AndroidDeviceImpl.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright.impl;
+
+import com.microsoft.playwright.AndroidDevice;
+import com.microsoft.playwright.PlaywrightException;
+import com.google.gson.JsonObject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import static com.microsoft.playwright.impl.Utils.isSafeCloseError;
+
+public class AndroidDeviceImpl extends ChannelOwner implements AndroidDevice {
+ final Set contexts = new HashSet<>();
+ boolean isConnectedOverWebSocket;
+ final TimeoutSettings timeoutSettings;
+ private final ListenerCollection listeners = new ListenerCollection<>();
+
+ enum EventType {
+ CLOSE,
+ }
+ AndroidDeviceImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
+ super(parent, type, guid, initializer);
+ this.timeoutSettings = new TimeoutSettings(((AndroidImpl) parent).timeoutSettings);
+ }
+
+ @Override
+ public void onClose(Consumer handler) {
+ listeners.add(EventType.CLOSE, handler);
+ }
+
+ @Override
+ public void offClose(Consumer handler) {
+ listeners.remove(EventType.CLOSE, handler);
+ }
+
+ @Override
+ public void close() {
+ withLogging("AndroidDevice.close", () -> closeImpl());
+ }
+
+ private void closeImpl() {
+ if (isConnectedOverWebSocket) {
+ try {
+ connection.close();
+ } catch (IOException e) {
+ throw new PlaywrightException("Failed to close device connection", e);
+ }
+ } else {
+ try {
+ sendMessage("close");
+ } catch (PlaywrightException e) {
+ if (!isSafeCloseError(e)) {
+ throw e;
+ }
+ }
+ }
+ }
+
+ void didClose() {
+ listeners.notify(EventType.CLOSE, this);
+ }
+
+ @Override
+ public String model() {
+ return initializer.get("model").getAsString();
+ }
+
+ void notifyRemoteClosed() {
+ for (BrowserContextImpl context : new ArrayList<>(contexts)) {
+ for (PageImpl page : new ArrayList<>(context.pages)) {
+ page.didClose();
+ }
+ context.didClose();
+ }
+ didClose();
+ }
+
+ @Override
+ public String serial() {
+ return initializer.get("serial").getAsString();
+ }
+
+ @Override
+ void handleEvent(String event, JsonObject parameters) {
+ if ("close".equals(event)) {
+ didClose();
+ }
+ }
+}
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/AndroidImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/AndroidImpl.java
new file mode 100644
index 000000000..df775e526
--- /dev/null
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/AndroidImpl.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) Microsoft Corporation.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.microsoft.playwright.impl;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.microsoft.playwright.Android;
+import com.microsoft.playwright.AndroidDevice;
+import com.microsoft.playwright.PlaywrightException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+import static com.microsoft.playwright.impl.Serialization.gson;
+
+public class AndroidImpl extends ChannelOwner implements Android {
+ final TimeoutSettings timeoutSettings = new TimeoutSettings();
+
+ public AndroidImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
+ super(parent, type, guid, initializer);
+ }
+
+ @Override
+ public void setDefaultTimeout(double timeout) {
+ setDefaultTimeoutImpl(timeout);
+ }
+
+ void setDefaultTimeoutImpl(Double timeout) {
+ withLogging("Android.setDefaultTimeout", () -> {
+ timeoutSettings.setDefaultTimeout(timeout);
+ JsonObject params = new JsonObject();
+ params.addProperty("timeout", timeout);
+ sendMessage("setDefaultTimeoutNoReply", params);
+ });
+ }
+
+ @Override
+ public List devices(DevicesOptions options) {
+ return withLogging("Android.devices", () -> devicesImpl(options));
+ }
+
+ private List devicesImpl(DevicesOptions options) {
+ if (options == null) {
+ options = new DevicesOptions();
+ }
+ JsonObject params = gson().toJsonTree(options).getAsJsonObject();
+ JsonElement result = sendMessage("devices", params);
+ List devices = new ArrayList<>();
+ for (JsonElement deviceElement : result.getAsJsonObject().get("devices").getAsJsonArray()) {
+ JsonObject deviceJson = deviceElement.getAsJsonObject();
+ AndroidDevice device = connection.getExistingObject(deviceJson.get("guid").getAsString());
+ devices.add(device);
+ }
+ return devices;
+ }
+
+ @Override
+ public AndroidDevice connect(String wsEndpoint, ConnectOptions options) {
+ return withLogging("Android.connect", () -> connectImpl(wsEndpoint, options));
+ }
+
+ private AndroidDevice connectImpl(String wsEndpoint, ConnectOptions options) {
+ if (options == null) {
+ options = new ConnectOptions();
+ }
+
+ if (options.headers != null && !options.headers.isEmpty()) {
+ options.headers.put("x-playwright-browser", "android");
+ }
+
+ JsonObject params = gson().toJsonTree(options).getAsJsonObject();
+ params.addProperty("wsEndpoint", wsEndpoint);
+ JsonObject json = connection.localUtils().sendMessage("connect", params).getAsJsonObject();
+ JsonPipe pipe = connection.getExistingObject(json.getAsJsonObject("pipe").get("guid").getAsString());
+ Connection androidConnection = new Connection(pipe, connection.env, connection.localUtils);
+ PlaywrightImpl playwright = androidConnection.initializePlaywright();
+
+ if (!playwright.initializer.has("preConnectedAndroidDevice")) {
+ try {
+ androidConnection.close();
+ } catch (IOException e) {
+ e.printStackTrace(System.err);
+ }
+ throw new PlaywrightException("Malformed endpoint. Did you use the correct wsEndpoint?");
+ }
+
+ JsonObject preConnectedDevice = playwright.initializer.getAsJsonObject("preConnectedAndroidDevice");
+ AndroidDeviceImpl androidDevice = androidConnection.getExistingObject(preConnectedDevice.get("guid").getAsString());
+ androidDevice.isConnectedOverWebSocket = true;
+ Consumer connectionCloseListener = t -> androidDevice.notifyRemoteClosed();
+ pipe.onClose(connectionCloseListener);
+ androidDevice.onClose(b -> {
+ pipe.offClose(connectionCloseListener);
+ try {
+ androidConnection.close();
+ } catch (IOException e) {
+ e.printStackTrace(System.err);
+ }
+ });
+ return androidDevice;
+ }
+}
+
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
index f0d3ca5f0..937fb82af 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/Connection.java
@@ -304,13 +304,13 @@ private ChannelOwner createRemoteObject(String parentGuid, JsonObject params) {
ChannelOwner result = null;
switch (type) {
case "Android":
-// result = new Android(parent, type, guid, initializer);
+ result = new AndroidImpl(parent, type, guid, initializer);
break;
case "AndroidSocket":
// result = new AndroidSocket(parent, type, guid, initializer);
break;
case "AndroidDevice":
-// result = new AndroidDevice(parent, type, guid, initializer);
+ result = new AndroidDeviceImpl(parent, type, guid, initializer);
break;
case "Artifact":
result = new ArtifactImpl(parent, type, guid, initializer);
diff --git a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java
index 38863c8e8..0abf1b6ae 100644
--- a/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java
+++ b/playwright/src/main/java/com/microsoft/playwright/impl/PlaywrightImpl.java
@@ -19,6 +19,7 @@
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.microsoft.playwright.APIRequest;
+import com.microsoft.playwright.Android;
import com.microsoft.playwright.Playwright;
import com.microsoft.playwright.PlaywrightException;
import com.microsoft.playwright.Selectors;
@@ -64,6 +65,7 @@ public static PlaywrightImpl createImpl(CreateOptions options, boolean forceNewD
private final BrowserTypeImpl webkit;
private final SelectorsImpl selectors;
private final APIRequestImpl apiRequest;
+ private final AndroidImpl android;
private SharedSelectors sharedSelectors;
PlaywrightImpl(ChannelOwner parent, String type, String guid, JsonObject initializer) {
@@ -71,7 +73,7 @@ public static PlaywrightImpl createImpl(CreateOptions options, boolean forceNewD
chromium = parent.connection.getExistingObject(initializer.getAsJsonObject("chromium").get("guid").getAsString());
firefox = parent.connection.getExistingObject(initializer.getAsJsonObject("firefox").get("guid").getAsString());
webkit = parent.connection.getExistingObject(initializer.getAsJsonObject("webkit").get("guid").getAsString());
-
+ android = parent.connection.getExistingObject(initializer.getAsJsonObject("android").get("guid").getAsString());
selectors = connection.getExistingObject(initializer.getAsJsonObject("selectors").get("guid").getAsString());
apiRequest = new APIRequestImpl(this);
}
@@ -123,6 +125,11 @@ public Selectors selectors() {
return sharedSelectors;
}
+ @Override
+ public Android android() {
+ return android;
+ }
+
@Override
public void close() {
try {
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestAndroid.java b/playwright/src/test/java/com/microsoft/playwright/TestAndroid.java
new file mode 100644
index 000000000..2bf5b069a
--- /dev/null
+++ b/playwright/src/test/java/com/microsoft/playwright/TestAndroid.java
@@ -0,0 +1,20 @@
+package com.microsoft.playwright;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+public class TestAndroid extends TestAndroidBase {
+ @Test
+ void testAndroidDeviceClose() {
+ List events = new ArrayList<>();
+ androidDevice.onClose(d -> {
+ events.add("close");
+ });
+ androidDevice.close();
+ assertEquals(Arrays.asList("close"), events);
+ }
+}
diff --git a/playwright/src/test/java/com/microsoft/playwright/TestAndroidBase.java b/playwright/src/test/java/com/microsoft/playwright/TestAndroidBase.java
new file mode 100644
index 000000000..e2cedf65b
--- /dev/null
+++ b/playwright/src/test/java/com/microsoft/playwright/TestAndroidBase.java
@@ -0,0 +1,59 @@
+package com.microsoft.playwright;
+
+import org.junit.jupiter.api.*;
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static com.microsoft.playwright.Utils.nextFreePort;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestAndroidBase {
+ Playwright playwright;
+ Android android;
+ AndroidDevice androidDevice;
+ Server server;
+ Server httpsServer;
+
+ @BeforeAll
+ void setupAndroid() {
+ playwright = Playwright.create();
+ List devices = playwright.android().devices();
+ assertFalse(devices.isEmpty());
+ androidDevice = devices.get(0);
+ }
+
+ @AfterAll
+ void teardownAndroid() {
+ if (androidDevice != null) {
+ androidDevice.close();
+ }
+ if (playwright != null) {
+ playwright.close();
+ }
+ }
+
+ @BeforeAll
+ void startServer() throws IOException {
+ server = Server.createHttp(nextFreePort());
+ httpsServer = Server.createHttps(nextFreePort());
+ }
+
+ @AfterAll
+ void stopServer() {
+ if (server != null) {
+ server.stop();
+ server = null;
+ }
+ if (httpsServer != null) {
+ httpsServer.stop();
+ httpsServer = null;
+ }
+ }
+
+ @AfterEach
+ void cleanupPage() {
+ server.reset();
+ httpsServer.reset();
+ }
+}