From c5079d6bfab0294d48f0e601de0aedac831e3e7b Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 5 Oct 2026 20:14:13 +0200 Subject: [PATCH 1/6] feat!: remove the deprecated AppiumFunction BREAKING CHANGE: io.appium.java_client.functions.AppiumFunction is removed. Use java.util.function.Function instead. Co-Authored-By: Claude Sonnet 5.5 --- .../android/AndroidFunctionTest.java | 18 +++---- .../java_client/functions/AppiumFunction.java | 49 ------------------- 2 files changed, 9 insertions(+), 58 deletions(-) delete mode 100644 src/main/java/io/appium/java_client/functions/AppiumFunction.java diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java index 45e55d4a8..514996ab9 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java @@ -1,6 +1,5 @@ package io.appium.java_client.android; -import io.appium.java_client.functions.AppiumFunction; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,6 +13,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -27,7 +27,7 @@ public class AndroidFunctionTest extends BaseAndroidTest { - private final AppiumFunction> searchingFunction = input -> { + private final Function> searchingFunction = input -> { List result = input.findElements(By.tagName("a")); if (result.size() > 0) { @@ -36,7 +36,7 @@ public class AndroidFunctionTest extends BaseAndroidTest { return null; }; - private final AppiumFunction contextFunction = input -> { + private final Function contextFunction = input -> { Set contexts = driver.getContextHandles(); String current = driver.getContext(); contexts.forEach(context -> { @@ -51,7 +51,7 @@ public class AndroidFunctionTest extends BaseAndroidTest { return null; }; - private final AppiumFunction, List> filteringFunction = input -> { + private final Function, List> filteringFunction = input -> { final List result = new ArrayList<>(); input.forEach(element -> { if (element.getText().equals("Hello World! - 1")) { @@ -80,7 +80,7 @@ public void setUp() { @Test public void complexWaitingTestWithPreCondition() { - AppiumFunction> compositeFunction = + Function> compositeFunction = searchingFunction.compose(contextFunction); Wait wait = new FluentWait<>(Pattern.compile("WEBVIEW")) @@ -94,7 +94,7 @@ public void complexWaitingTestWithPreCondition() { @Test public void complexWaitingTestWithPostConditions() { final List calls = new ArrayList<>(); - AppiumFunction waitingForContext = input -> { + Function waitingForContext = input -> { WebDriver result = contextFunction.apply(input); if (result != null) { calls.add(true); @@ -102,15 +102,15 @@ public void complexWaitingTestWithPreCondition() { return result; }; - AppiumFunction> compositeFunction = waitingForContext - .andThen((AppiumFunction>) input -> { + Function> compositeFunction = waitingForContext + .andThen((Function>) input -> { List result = searchingFunction.apply(input); if (result != null) { calls.add(true); } return result; }) - .andThen((AppiumFunction, List>) input -> { + .andThen((Function, List>) input -> { List result = filteringFunction.apply(input); if (result != null) { calls.add(true); diff --git a/src/main/java/io/appium/java_client/functions/AppiumFunction.java b/src/main/java/io/appium/java_client/functions/AppiumFunction.java deleted file mode 100644 index 792b0c412..000000000 --- a/src/main/java/io/appium/java_client/functions/AppiumFunction.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * 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 io.appium.java_client.functions; - -import java.util.Objects; -import java.util.Optional; - -/** - * This is extended version of {@link java.util.function.Function}. It was designed in order to provide - * compatibility with the {@link io.appium.java_client.support.ui.Wait}. - * - * @param The input type - * @param The return type - * @deprecated Use {@link java.util.function.Function} instead - */ -@Deprecated -@FunctionalInterface -public interface AppiumFunction extends java.util.function.Function { - - @Override default AppiumFunction compose(java.util.function.Function before) { - Objects.requireNonNull(before); - return (V v) -> { - F f = before.apply(v); - return Optional.ofNullable(f != null ? apply(f) : null).orElse(null); - }; - } - - @Override default AppiumFunction andThen(java.util.function.Function after) { - Objects.requireNonNull(after); - return (F f) -> { - T result = apply(f); - return Optional.ofNullable(result != null ? after.apply(result) : null).orElse(null); - }; - } -} From a1ecb29b554d3d18df4f7d67588d42e953967b30 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 5 Oct 2026 20:14:13 +0200 Subject: [PATCH 2/6] fix: report the error of a failed storage upload The events of a failed upload have no success field, so the null Boolean cast threw and the error was wrapped into a generic exception with the raw payload as its message. Check it null-safely so that the error sent by the server is decoded. Co-Authored-By: Claude Sonnet 5.5 --- .../io/appium/java_client/plugins/storage/StorageClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java b/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java index 083f1d0c3..8529c0036 100644 --- a/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java +++ b/src/main/java/io/appium/java_client/plugins/storage/StorageClient.java @@ -243,7 +243,7 @@ private Optional extractException(String payload) { Map record = gson.fromJson(payload, MAP_TYPE); //noinspection unchecked var value = (Map) record.get("value"); - if ((Boolean) value.get("success")) { + if (Boolean.TRUE.equals(value.get("success"))) { return Optional.empty(); } return Optional.of(ErrorCodec.decode(record)); From c8a9f7c5a69c71f705104016d7b9412d437c645f Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 5 Oct 2026 20:14:13 +0200 Subject: [PATCH 3/6] test: run the page factory and storage tests without real servers DesktopBrowserCompatibilityTest and TimeoutTest use the stub driver instead of headless Chrome, and TimeoutTest now asserts a lower bound of the waiting time and runs in 1.5s instead of about 12s. StorageTest runs against a fake of the storage plugin of the Appium server rather than a server at 127.0.0.1:4723, so it is not skipped on CI anymore. The selenium-chrome-driver and webdrivermanager dependencies of the unit test suite are removed, along with the deprecated timeout overloads of the stub driver. Co-Authored-By: Claude Sonnet 5.5 --- build.gradle | 6 - .../DesktopBrowserCompatibilityTest.java | 37 +-- .../pagefactory_tests/TimeoutTest.java | 91 ++---- .../widget/tests/AbstractStubWebDriver.java | 63 +--- .../java_client/plugin/FakeStorageServer.java | 287 ++++++++++++++++++ .../java_client/plugin/StorageTest.java | 62 +++- 6 files changed, 372 insertions(+), 174 deletions(-) create mode 100644 src/test/java/io/appium/java_client/plugin/FakeStorageServer.java diff --git a/build.gradle b/build.gradle index 9687144ef..db49632fd 100644 --- a/build.gradle +++ b/build.gradle @@ -235,12 +235,6 @@ testing { } test { - dependencies { - implementation "org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}" - implementation('io.github.bonigarcia:webdrivermanager:6.4.0') { - exclude group: 'org.seleniumhq.selenium' - } - } targets.configureEach { testTask.configure { finalizedBy jacocoTestReport diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java index 21bee8ff2..4d3aba0c3 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/DesktopBrowserCompatibilityTest.java @@ -22,30 +22,23 @@ import io.appium.java_client.pagefactory.HowToUseLocators; import io.appium.java_client.pagefactory.Widget; import io.appium.java_client.pagefactory.iOSXCUITFindBy; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver.StubBrowserDriver; import io.appium.java_client.support.FindBy; import io.appium.java_client.support.FindBys; import io.appium.java_client.support.PageFactory; -import io.appium.java_client.utils.TestUtils; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeOptions; import java.util.List; import static io.appium.java_client.pagefactory.LocatorGroupStrategy.ALL_POSSIBLE; -import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; import static java.time.Duration.ofSeconds; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNull; public class DesktopBrowserCompatibilityTest { - private static final String HELLO_APPIUM_HTML = - TestUtils.resourcePathToAbsolutePath("html/hello appium - saved page.htm").toUri().toString(); - @HowToUseLocators(iOSXCUITAutomation = ALL_POSSIBLE) @AndroidFindBy(className = "someClass") @iOSXCUITFindBy(xpath = "//selector[1]") @iOSXCUITFindBy(xpath = "//someTag") @@ -55,25 +48,13 @@ public class DesktopBrowserCompatibilityTest { private WebDriver trap1; private List trap2; - /** - * The starting. - */ - @BeforeAll public static void beforeClass() { - chromedriver().setup(); - } - - @Test public void chromeTest() { - WebDriver driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new")); - try { - PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(15)), this); - driver.get(HELLO_APPIUM_HTML); - assertNotEquals(0, foundLinks.size()); - assertNotEquals(0, main.size()); - assertNull(trap1); - assertNull(trap2); - foundLinks.forEach(element -> assertFalse(Widget.class.isAssignableFrom(element.getClass()))); - } finally { - driver.quit(); - } + @Test public void browserDriverTest() { + WebDriver driver = new StubBrowserDriver(); + PageFactory.initElements(new AppiumFieldDecorator(driver, ofSeconds(15)), this); + assertNotEquals(0, foundLinks.size()); + assertNotEquals(0, main.size()); + assertNull(trap1); + assertNull(trap2); + foundLinks.forEach(element -> assertFalse(Widget.class.isAssignableFrom(element.getClass()))); } } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java index cfb3deadc..a8f425753 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/TimeoutTest.java @@ -18,104 +18,63 @@ import io.appium.java_client.pagefactory.AppiumFieldDecorator; import io.appium.java_client.pagefactory.WithTimeout; +import io.appium.java_client.pagefactory_tests.widget.tests.AbstractStubWebDriver; import io.appium.java_client.support.FindAll; import io.appium.java_client.support.FindBy; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; -import org.openqa.selenium.chrome.ChromeDriver; -import org.openqa.selenium.chrome.ChromeOptions; import java.time.Duration; import java.util.List; -import static io.appium.java_client.pagefactory.AppiumFieldDecorator.DEFAULT_WAITING_TIMEOUT; import static io.appium.java_client.support.PageFactory.initElements; -import static io.github.bonigarcia.wdm.WebDriverManager.chromedriver; -import static java.lang.Math.abs; -import static java.lang.String.format; -import static java.lang.System.currentTimeMillis; -import static java.time.Duration.ofSeconds; -import static java.time.temporal.ChronoUnit.SECONDS; -import static org.apache.commons.lang3.time.DurationFormatUtils.formatDuration; +import static java.time.Duration.ofMillis; +import static java.time.temporal.ChronoUnit.MILLIS; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThanOrEqualTo; public class TimeoutTest { - private static final long ACCEPTABLE_TIME_DIFF_MS = 1500; - - private WebDriver driver; + private static final Duration DEFAULT_TIMEOUT = ofMillis(300); + private static final long CLOCK_SLACK_MS = 50; + private static final long ACCEPTABLE_OVERRUN_MS = 1500; @FindAll({ @FindBy(className = "ClassWhichDoesNotExist"), @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) private List stubElements; - @WithTimeout(time = 5, chronoUnit = SECONDS) + @WithTimeout(time = 800, chronoUnit = MILLIS) @FindAll({@FindBy(className = "ClassWhichDoesNotExist"), @FindBy(className = "OneAnotherClassWhichDoesNotExist")}) private List stubElements2; - private Duration timeOutDuration; - - private static long getExpectedMillis(Duration duration) { - return duration.toMillis(); - } - - private static long getPerformanceDiff(long expectedMs, Runnable runnable) { - long startMark = currentTimeMillis(); + private static void assertWaitsFor(Duration expected, Runnable runnable) { + long start = System.nanoTime(); runnable.run(); - long endMark = currentTimeMillis(); - return abs(expectedMs - (endMark - startMark)); - } - - private static String assertionMessage(Duration expectedDuration) { - return format("Check difference from the expected waiting duration %s", - formatDuration(expectedDuration.toMillis(), "H:mm:ss:SSS", true)); - } - - @BeforeAll - public static void beforeAll() { - chromedriver().setup(); + long elapsedMs = Duration.ofNanos(System.nanoTime() - start).toMillis(); + assertThat("The lookup should wait for " + expected, elapsedMs, + greaterThanOrEqualTo(expected.toMillis() - CLOCK_SLACK_MS)); + assertThat("The lookup should not wait much longer than " + expected, elapsedMs, + lessThanOrEqualTo(expected.toMillis() + ACCEPTABLE_OVERRUN_MS)); } - /** - * The setting up. - */ @BeforeEach public void setUp() { - driver = new ChromeDriver(new ChromeOptions().addArguments("--headless=new")); - timeOutDuration = DEFAULT_WAITING_TIMEOUT; - initElements(new AppiumFieldDecorator(driver, timeOutDuration), this); - } - - /** - * finishing. - */ - @AfterEach public void tearDown() { - driver.quit(); + WebDriver driver = new AbstractStubWebDriver() { + @Override + public List findElements(By by) { + return List.of(); + } + }; + initElements(new AppiumFieldDecorator(driver, DEFAULT_TIMEOUT), this); } @Test public void withCustomizedTimeOutTest() { - assertThat(assertionMessage(DEFAULT_WAITING_TIMEOUT), - getPerformanceDiff(getExpectedMillis(DEFAULT_WAITING_TIMEOUT), () -> stubElements.size()), - lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); - - assertThat(assertionMessage(ofSeconds(5)), - getPerformanceDiff(getExpectedMillis(ofSeconds(5)), () -> stubElements2.size()), - lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); - - timeOutDuration.plus(ofSeconds(10)); - - assertThat(assertionMessage(timeOutDuration), - getPerformanceDiff(getExpectedMillis(timeOutDuration), () -> stubElements.size()), - lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); - - assertThat(assertionMessage(ofSeconds(5)), - getPerformanceDiff(getExpectedMillis(ofSeconds(5)), () -> stubElements2.size()), - lessThanOrEqualTo(ACCEPTABLE_TIME_DIFF_MS)); + assertWaitsFor(DEFAULT_TIMEOUT, () -> stubElements.size()); + assertWaitsFor(ofMillis(800), () -> stubElements2.size()); } } diff --git a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java index 31118a8c1..8c13dd102 100644 --- a/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java +++ b/src/test/java/io/appium/java_client/pagefactory_tests/widget/tests/AbstractStubWebDriver.java @@ -16,14 +16,12 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; import static io.appium.java_client.remote.AutomationName.ANDROID_UIAUTOMATOR2; import static io.appium.java_client.remote.AutomationName.IOS_XCUI_TEST; import static io.appium.java_client.remote.MobilePlatform.ANDROID; import static io.appium.java_client.remote.MobilePlatform.IOS; import static io.appium.java_client.remote.MobilePlatform.WINDOWS; -import static org.apache.commons.lang3.StringUtils.EMPTY; public abstract class AbstractStubWebDriver implements WebDriver, @@ -150,75 +148,20 @@ public Set getCookies() { @Override public Cookie getCookieNamed(String name) { - return new Cookie(name, EMPTY); + return new Cookie(name, ""); } @Override public Timeouts timeouts() { return new Timeouts() { - /** - * Does nothing. - * - * @param time The amount of time to wait. - * @param unit The unit of measure for {@code time}. - * @return A self reference. - * @deprecated Kept for the backward compatibility, should be removed when a minimum Selenium - * version is bumped to 4.33.0 or higher. - */ - @Deprecated - public Timeouts implicitlyWait(long time, TimeUnit unit) { - return this; - } - public Timeouts implicitlyWait(Duration duration) { return this; } - /** - * Does nothing. - * - * @param time The timeout value. - * @param unit The unit of time. - * @return A self reference. - * @deprecated Kept for the backward compatibility, should be removed when Selenium client removes - * this method from its interface. - */ - @Deprecated - public Timeouts setScriptTimeout(long time, TimeUnit unit) { - return this; - } - - /** - * Does nothing. - * - * @param duration The timeout value. - * @return A self reference. - * @deprecated Kept for the backward compatibility, should be removed when Selenium client removes - * this method from its interface. - */ - @Deprecated - public Timeouts setScriptTimeout(Duration duration) { - return this; - } - public Timeouts scriptTimeout(Duration duration) { return this; } - /** - * Does nothing. - * - * @param time The timeout value. - * @param unit The unit of time. - * @return A self reference. - * @deprecated Kept for the backward compatibility, should be removed when Selenium client removes - * this method from its interface. - */ - @Deprecated - public Timeouts pageLoadTimeout(long time, TimeUnit unit) { - return this; - } - public Timeouts pageLoadTimeout(Duration duration) { return this; } @@ -280,12 +223,12 @@ public static class StubBrowserDriver extends AbstractStubWebDriver { @Override public String getPlatformName() { - return EMPTY; + return ""; } @Override public String getAutomationName() { - return EMPTY; + return ""; } } diff --git a/src/test/java/io/appium/java_client/plugin/FakeStorageServer.java b/src/test/java/io/appium/java_client/plugin/FakeStorageServer.java new file mode 100644 index 000000000..4a198b085 --- /dev/null +++ b/src/test/java/io/appium/java_client/plugin/FakeStorageServer.java @@ -0,0 +1,287 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.plugin; + +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; + +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Type; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Imitates the storage plugin of the Appium server: the HTTP endpoints and the upload web sockets. + */ +final class FakeStorageServer implements AutoCloseable { + private static final String GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + private static final String UPLOAD_PREFIX = "/storage/add/"; + private static final long EVENTS_TIMEOUT_SEC = 10; + + private static final Type MAP_TYPE = new TypeToken>() { }.getType(); + + private final Gson gson = new Gson(); + private final ServerSocket serverSocket; + private final Map items = new LinkedHashMap<>(); + private final Map uploads = new ConcurrentHashMap<>(); + private final AtomicInteger uploadCounter = new AtomicInteger(); + private volatile boolean rejectUploads; + + private static final class Upload { + private final String name; + private final String sha1; + private final CompletableFuture events = new CompletableFuture<>(); + + private Upload(String name, String sha1) { + this.name = name; + this.sha1 = sha1; + } + } + + FakeStorageServer() throws IOException { + serverSocket = new ServerSocket(0, 50, InetAddress.getLoopbackAddress()); + var thread = new Thread(this::acceptConnections, "fake-storage-server"); + thread.setDaemon(true); + thread.start(); + } + + URL url() throws IOException { + return new URL("http://127.0.0.1:" + serverSocket.getLocalPort()); + } + + /** Makes the server report a failure for the uploads, as if the content could not be saved. */ + void rejectUploads() { + rejectUploads = true; + } + + synchronized byte[] content(String name) { + return items.get(name); + } + + @Override + public void close() throws IOException { + serverSocket.close(); + } + + private void acceptConnections() { + while (!serverSocket.isClosed()) { + try { + var socket = serverSocket.accept(); + var thread = new Thread(() -> handle(socket), "fake-storage-connection"); + thread.setDaemon(true); + thread.start(); + } catch (IOException e) { + return; + } + } + } + + private void handle(Socket socket) { + try (socket) { + var in = new DataInputStream(socket.getInputStream()); + var out = socket.getOutputStream(); + var head = readHead(in); + var lines = head.split("\r\n"); + var requestLine = lines[0].split(" "); + var headers = new LinkedHashMap(); + for (int i = 1; i < lines.length; i++) { + int colon = lines[i].indexOf(':'); + headers.put(lines[i].substring(0, colon).toLowerCase(), lines[i].substring(colon + 1).trim()); + } + if ("websocket".equalsIgnoreCase(headers.get("upgrade"))) { + handleWebSocket(requestLine[1], headers.get("sec-websocket-key"), in, out); + return; + } + var body = new byte[Integer.parseInt(headers.getOrDefault("content-length", "0"))]; + in.readFully(body); + respond(out, requestLine[0], requestLine[1], new String(body, StandardCharsets.UTF_8)); + } catch (IOException | NoSuchAlgorithmException | RuntimeException e) { + // the client has gone away, nothing to report + } + } + + private void respond(OutputStream out, String method, String path, String body) throws IOException { + var route = method + " " + path; + String json; + switch (route) { + case "POST /storage/reset": + synchronized (this) { + items.clear(); + } + json = "{\"value\":null}"; + break; + case "GET /storage/list": + json = gson.toJson(Map.of("value", list())); + break; + case "POST /storage/delete": + Map deleteArgs = gson.fromJson(body, MAP_TYPE); + boolean deleted; + synchronized (this) { + deleted = items.remove((String) deleteArgs.get("name")) != null; + } + json = "{\"value\":" + deleted + "}"; + break; + case "POST /storage/add": + json = gson.toJson(Map.of("value", startUpload(body))); + break; + default: + writeResponse(out, 404, "{\"value\":{\"error\":\"unknown command\",\"message\":\"" + route + + "\",\"stacktrace\":\"\"}}"); + return; + } + writeResponse(out, 200, json); + } + + private synchronized List> list() { + var result = new ArrayList>(); + items.forEach((name, content) -> result.add( + Map.of("name", name, "path", "/storage/" + name, "size", (long) content.length))); + return result; + } + + private Map startUpload(String body) { + Map args = gson.fromJson(body, MAP_TYPE); + var id = String.valueOf(uploadCounter.incrementAndGet()); + uploads.put(id, new Upload((String) args.get("name"), (String) args.get("sha1"))); + return Map.of( + "ws", Map.of("stream", UPLOAD_PREFIX + id + "/stream", "events", UPLOAD_PREFIX + id + "/events"), + "ttlMs", 10000L + ); + } + + private void handleWebSocket(String path, String key, DataInputStream in, OutputStream out) + throws IOException, NoSuchAlgorithmException { + var accept = Base64.getEncoder().encodeToString(MessageDigest.getInstance("SHA-1") + .digest((key + GUID).getBytes(StandardCharsets.US_ASCII))); + out.write(("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n" + + "Sec-WebSocket-Accept: " + accept + "\r\n\r\n").getBytes(StandardCharsets.US_ASCII)); + out.flush(); + var parts = path.substring(UPLOAD_PREFIX.length()).split("/"); + var upload = uploads.get(parts[0]); + if ("events".equals(parts[1])) { + upload.events.complete(out); + while (readFrame(in, new ByteArrayOutputStream()) != 0x8) { + // the events socket receives nothing but the close frame + } + return; + } + var content = new ByteArrayOutputStream(); + while (readFrame(in, content) != 0x8) { + // collecting the chunks until the client closes the stream + } + try { + var events = upload.events.get(EVENTS_TIMEOUT_SEC, TimeUnit.SECONDS); + writeFrame(events, 0x1, uploadResult(upload, content.toByteArray()).getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { + throw new IOException(e); + } + writeFrame(out, 0x8, new byte[]{0x03, (byte) 0xe8}); + } + + private String uploadResult(Upload upload, byte[] content) throws NoSuchAlgorithmException { + var actualSha1 = new StringBuilder(); + for (byte b : MessageDigest.getInstance("SHA-1").digest(content)) { + actualSha1.append(String.format("%02x", b)); + } + if (rejectUploads || !actualSha1.toString().equals(upload.sha1)) { + return "{\"value\":{\"error\":\"unknown error\",\"message\":\"The upload of '" + upload.name + + "' has failed\",\"stacktrace\":\"\"}}"; + } + synchronized (this) { + items.put(upload.name, content); + } + return gson.toJson(Map.of("value", Map.of("success", true, "name", upload.name, "sha1", upload.sha1))); + } + + private static String readHead(InputStream in) throws IOException { + var buffer = new ByteArrayOutputStream(); + while (!buffer.toString(StandardCharsets.US_ASCII).endsWith("\r\n\r\n")) { + int b = in.read(); + if (b < 0) { + break; + } + buffer.write(b); + } + return buffer.toString(StandardCharsets.US_ASCII); + } + + private static void writeResponse(OutputStream out, int status, String json) throws IOException { + var bytes = json.getBytes(StandardCharsets.UTF_8); + out.write(("HTTP/1.1 " + status + " X\r\nContent-Type: application/json; charset=utf-8\r\n" + + "Content-Length: " + bytes.length + "\r\nConnection: close\r\n\r\n") + .getBytes(StandardCharsets.US_ASCII)); + out.write(bytes); + out.flush(); + } + + /** Reads a client frame, appends the payload of the binary ones to the sink and returns the opcode. */ + private static int readFrame(DataInputStream in, ByteArrayOutputStream sink) throws IOException { + final int opcode = in.readUnsignedByte() & 0x0f; + int lengthByte = in.readUnsignedByte(); + boolean masked = (lengthByte & 0x80) != 0; + long length = lengthByte & 0x7f; + if (length == 126) { + length = in.readUnsignedShort(); + } else if (length == 127) { + length = in.readLong(); + } + var mask = new byte[4]; + if (masked) { + in.readFully(mask); + } + var payload = new byte[(int) length]; + in.readFully(payload); + if (opcode == 0x2 || opcode == 0x0) { + for (int i = 0; i < payload.length; i++) { + payload[i] ^= mask[i % 4]; + } + sink.write(payload); + } + return opcode; + } + + private static synchronized void writeFrame(OutputStream out, int opcode, byte[] payload) throws IOException { + out.write(0x80 | opcode); + if (payload.length < 126) { + out.write(payload.length); + } else { + out.write(126); + out.write(payload.length >> 8); + out.write(payload.length); + } + out.write(payload); + out.flush(); + } +} diff --git a/src/test/java/io/appium/java_client/plugin/StorageTest.java b/src/test/java/io/appium/java_client/plugin/StorageTest.java index a12708dc7..caea4fcc9 100644 --- a/src/test/java/io/appium/java_client/plugin/StorageTest.java +++ b/src/test/java/io/appium/java_client/plugin/StorageTest.java @@ -18,36 +18,54 @@ import io.appium.java_client.plugins.storage.StorageClient; import io.appium.java_client.utils.TestUtils; -import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openqa.selenium.WebDriverException; -import java.net.MalformedURLException; -import java.net.URL; +import java.io.IOException; +import java.nio.file.Files; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +/** + * Runs the storage client against a fake of the storage plugin of the Appium server. + */ public class StorageTest { + private static final String NAME = "hello appium - saved page.htm"; + + private FakeStorageServer server; private StorageClient storageClient; @BeforeEach - void before() throws MalformedURLException { - // These tests assume Appium server with storage plugin is already running - // at the given baseUrl - Assumptions.assumeFalse(TestUtils.isCiEnv()); - storageClient = new StorageClient(new URL("http://127.0.0.1:4723")); + void before() throws IOException { + server = new FakeStorageServer(); + storageClient = new StorageClient(server.url()); storageClient.reset(); } + @AfterEach + void after() throws IOException { + server.close(); + } + @Test - void shouldBeAbleToPerformBasicStorageActions() { + void shouldBeAbleToPerformBasicStorageActions() throws IOException { assertTrue(storageClient.list().isEmpty()); - var name = "hello appium - saved page.htm"; - var testFile = TestUtils.resourcePathToAbsolutePath("html/" + name).toFile(); + var testFile = TestUtils.resourcePathToAbsolutePath("html/" + NAME).toFile(); storageClient.add(testFile); assertItemsCount(1); - assertTrue(storageClient.delete(name)); + var item = storageClient.list().get(0); + assertEquals(NAME, item.getName()); + assertEquals(testFile.length(), item.getSize()); + assertArrayEquals(Files.readAllBytes(testFile.toPath()), server.content(NAME)); + assertTrue(storageClient.delete(NAME)); + assertFalse(storageClient.delete(NAME)); assertItemsCount(0); storageClient.add(testFile); assertItemsCount(1); @@ -55,8 +73,24 @@ void shouldBeAbleToPerformBasicStorageActions() { assertItemsCount(0); } + @Test + void shouldAddAnItemUnderTheGivenName() { + var testFile = TestUtils.resourcePathToAbsolutePath("html/" + NAME).toFile(); + storageClient.add(testFile, "renamed.htm"); + assertEquals("renamed.htm", storageClient.list().get(0).getName()); + } + + @Test + void shouldFailIfTheServerReportsAFailedUpload() { + var testFile = TestUtils.resourcePathToAbsolutePath("html/" + NAME).toFile(); + server.rejectUploads(); + var error = assertThrows(WebDriverException.class, () -> storageClient.add(testFile)); + assertTrue(error.getRawMessage().startsWith("The upload of '" + NAME + "' has failed"), error.getMessage()); + assertNull(error.getCause()); + assertItemsCount(0); + } + private void assertItemsCount(int expected) { - var items = storageClient.list(); - assertEquals(expected, items.size()); + assertEquals(expected, storageClient.list().size()); } } From e04c16f1554191a630ed6930c4c940d980e63bad Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 5 Oct 2026 20:29:00 +0200 Subject: [PATCH 4/6] feat!: remove the deprecated legacy mobile commands and their fallbacks The drivers call the `mobile:` extensions only, as the legacy endpoints were removed in Appium 3 or have extension replacements in the UiAutomator2, XCUITest and Windows drivers. The extension presence tracking served the fallbacks only, so it is removed as well. WindowsDriver keeps the driver-level file transfer commands, because the Windows driver has no extensions for them, and uses the `windows:` extensions to launch and close the app. The power AC extension is requested under its actual name, `mobile: powerAc`, so it is not routed to the removed endpoint anymore. BREAKING CHANGE: AndroidMobileCommandHelper, IOSMobileCommandHelper, CanRememberExtensionPresence and the deprecated MobileCommand constants and helper methods are removed. Android and iOS drivers require a server and drivers that support the corresponding `mobile:` extensions. Co-Authored-By: Claude Sonnet 5.5 --- .../io/appium/java_client/AppiumDriver.java | 19 - .../CanRememberExtensionPresence.java | 25 - .../io/appium/java_client/HasAppStrings.java | 37 +- .../java_client/HasOnScreenKeyboard.java | 14 +- .../io/appium/java_client/HidesKeyboard.java | 13 +- .../java_client/HidesKeyboardWithKeyName.java | 15 +- .../appium/java_client/InteractsWithApps.java | 170 ++----- .../io/appium/java_client/LocksDevice.java | 44 +- .../io/appium/java_client/MobileCommand.java | 441 +----------------- .../io/appium/java_client/PullsFiles.java | 44 +- .../io/appium/java_client/PushesFiles.java | 19 +- .../android/AndroidMobileCommandHelper.java | 355 -------------- .../android/AuthenticatesByFinger.java | 17 +- .../android/CanReplaceElementValue.java | 25 +- .../android/HasAndroidClipboard.java | 11 +- .../android/HasAndroidDeviceDetails.java | 21 +- .../java_client/android/HasNotifications.java | 13 +- .../HasSupportedPerformanceDataType.java | 31 +- .../java_client/android/StartsActivity.java | 29 +- .../android/SupportsGpsStateManagement.java | 12 +- .../SupportsNetworkStateManagement.java | 58 +-- .../SupportsSpecialEmulatorCommands.java | 132 +----- .../connection/HasNetworkConnection.java | 58 +-- .../android/nativekey/PressesKey.java | 32 +- .../java_client/clipboard/HasClipboard.java | 20 +- .../ios/IOSMobileCommandHelper.java | 61 --- .../appium/java_client/ios/ShakesDevice.java | 13 +- .../java_client/windows/WindowsDriver.java | 55 ++- .../remote/ExtensionCommandsOverHttpTest.java | 145 ++++++ src/test/resources/wire-golden/commands.json | 396 ---------------- 30 files changed, 389 insertions(+), 1936 deletions(-) delete mode 100644 src/main/java/io/appium/java_client/CanRememberExtensionPresence.java delete mode 100644 src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java delete mode 100644 src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java create mode 100644 src/test/java/io/appium/java_client/remote/ExtensionCommandsOverHttpTest.java diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index a45b74b1c..a36a1e35d 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -36,16 +36,13 @@ import org.openqa.selenium.ImmutableCapabilities; import org.openqa.selenium.OutputType; import org.openqa.selenium.SessionNotCreatedException; -import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriverException; import java.net.URL; import java.util.Arrays; import java.util.Collections; -import java.util.HashSet; import java.util.Map; import java.util.Optional; -import java.util.Set; import static io.appium.java_client.internal.CapabilityHelpers.APPIUM_PREFIX; import static io.appium.java_client.internal.Strings.isNullOrEmpty; @@ -63,7 +60,6 @@ public class AppiumDriver extends AppiumRemoteWebDriver implements ExecutesDriverScript, LogsEvents, HasBrowserCheck, - CanRememberExtensionPresence, HasSettings { private static final ErrorHandler ERROR_HANDLER = new ErrorHandler(new ErrorCodesMobile()); @@ -71,7 +67,6 @@ public class AppiumDriver extends AppiumRemoteWebDriver implements @Getter private final URL remoteAddress; private final ExecuteMethod executeMethod; - private final Set absentExtensionNames = new HashSet<>(); /** * Creates a new instance based on command {@code executor} and {@code capabilities}. @@ -238,20 +233,6 @@ public X convertFromPngBytes(byte[] png) { }); } - @Override - public AppiumDriver assertExtensionExists(String extName) { - if (absentExtensionNames.contains(extName)) { - throw new UnsupportedCommandException(); - } - return this; - } - - @Override - public AppiumDriver markExtensionAbsence(String extName) { - absentExtensionNames.add(extName); - return this; - } - protected HttpClient getHttpClient() { return ((AppiumCommandExecutor) getCommandExecutor()).getClient(); } diff --git a/src/main/java/io/appium/java_client/CanRememberExtensionPresence.java b/src/main/java/io/appium/java_client/CanRememberExtensionPresence.java deleted file mode 100644 index 36cd4b903..000000000 --- a/src/main/java/io/appium/java_client/CanRememberExtensionPresence.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.appium.java_client; - -import org.openqa.selenium.UnsupportedCommandException; - -public interface CanRememberExtensionPresence { - /** - * Verifies if the given extension is not present in the list of absent extensions - * for the given driver instance. - * This API is designed for private usage. - * - * @param extName extension name. - * @return self instance for chaining. - * @throws UnsupportedCommandException if the extension is listed in the list of absents. - */ - ExecutesMethod assertExtensionExists(String extName); - - /** - * Marks the given extension as absent for the given driver instance. - * This API is designed for private usage. - * - * @param extName extension name. - * @return self instance for chaining. - */ - ExecutesMethod markExtensionAbsence(String extName); -} diff --git a/src/main/java/io/appium/java_client/HasAppStrings.java b/src/main/java/io/appium/java_client/HasAppStrings.java index 1224b26f9..3fe9c9879 100644 --- a/src/main/java/io/appium/java_client/HasAppStrings.java +++ b/src/main/java/io/appium/java_client/HasAppStrings.java @@ -16,13 +16,9 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - import java.util.Map; -import static io.appium.java_client.MobileCommand.GET_STRINGS; - -public interface HasAppStrings extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasAppStrings extends ExecutesMethod { /** * Get all defined Strings from an app for the default language. * See the documentation for 'mobile: getAppStrings' extension for more details. @@ -31,12 +27,7 @@ public interface HasAppStrings extends ExecutesMethod, CanRememberExtensionPrese */ default Map getAppStringMap() { final String extName = "mobile: getAppStrings"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute(markExtensionAbsence(extName), GET_STRINGS); - } + return CommandExecutionHelper.executeScript(this, extName); } /** @@ -48,17 +39,9 @@ default Map getAppStringMap() { */ default Map getAppStringMap(String language) { final String extName = "mobile: getAppStrings"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "language", language - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(GET_STRINGS, Map.of("language", language)) - ); - } + return CommandExecutionHelper.executeScript(this, extName, Map.of( + "language", language + )); } /** @@ -76,15 +59,7 @@ default Map getAppStringMap(String language, String stringFile) "language", language, "stringFile", stringFile ); - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(GET_STRINGS, args) - ); - } + return CommandExecutionHelper.executeScript(this, extName, args); } } diff --git a/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java b/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java index b242d2b01..aeeb80054 100644 --- a/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java +++ b/src/main/java/io/appium/java_client/HasOnScreenKeyboard.java @@ -1,11 +1,8 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - -import static io.appium.java_client.MobileCommand.isKeyboardShownCommand; import static java.util.Objects.requireNonNull; -public interface HasOnScreenKeyboard extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasOnScreenKeyboard extends ExecutesMethod { /** * Check if the on-screen keyboard is displayed. @@ -15,13 +12,6 @@ public interface HasOnScreenKeyboard extends ExecutesMethod, CanRememberExtensio */ default boolean isKeyboardShown() { final String extName = "mobile: isKeyboardShown"; - try { - return requireNonNull(CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName)); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return requireNonNull( - CommandExecutionHelper.execute(markExtensionAbsence(extName), isKeyboardShownCommand()) - ); - } + return requireNonNull(CommandExecutionHelper.executeScript(this, extName)); } } diff --git a/src/main/java/io/appium/java_client/HidesKeyboard.java b/src/main/java/io/appium/java_client/HidesKeyboard.java index a6f522102..b53af5c9a 100644 --- a/src/main/java/io/appium/java_client/HidesKeyboard.java +++ b/src/main/java/io/appium/java_client/HidesKeyboard.java @@ -16,11 +16,7 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - -import static io.appium.java_client.MobileCommand.HIDE_KEYBOARD; - -public interface HidesKeyboard extends ExecutesMethod, CanRememberExtensionPresence { +public interface HidesKeyboard extends ExecutesMethod { /** * Hides the keyboard if it is showing. @@ -31,11 +27,6 @@ public interface HidesKeyboard extends ExecutesMethod, CanRememberExtensionPrese */ default void hideKeyboard() { final String extName = "mobile: hideKeyboard"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), HIDE_KEYBOARD); - } + CommandExecutionHelper.executeScript(this, extName); } } diff --git a/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java b/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java index c2a84bb11..b2c58bd93 100644 --- a/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java +++ b/src/main/java/io/appium/java_client/HidesKeyboardWithKeyName.java @@ -16,13 +16,9 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - import java.util.List; import java.util.Map; -import static io.appium.java_client.MobileCommand.hideKeyboardCommand; - public interface HidesKeyboardWithKeyName extends HidesKeyboard { /** @@ -37,13 +33,8 @@ public interface HidesKeyboardWithKeyName extends HidesKeyboard { */ default void hideKeyboard(String keyName) { final String extName = "mobile: hideKeyboard"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "keys", List.of(keyName) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), hideKeyboardCommand(keyName)); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "keys", List.of(keyName) + )); } } diff --git a/src/main/java/io/appium/java_client/InteractsWithApps.java b/src/main/java/io/appium/java_client/InteractsWithApps.java index 0ca018abb..79025d5a9 100644 --- a/src/main/java/io/appium/java_client/InteractsWithApps.java +++ b/src/main/java/io/appium/java_client/InteractsWithApps.java @@ -23,25 +23,16 @@ import io.appium.java_client.appmanagement.BaseRemoveApplicationOptions; import io.appium.java_client.appmanagement.BaseTerminateApplicationOptions; import org.jspecify.annotations.Nullable; -import org.openqa.selenium.InvalidArgumentException; -import org.openqa.selenium.UnsupportedCommandException; import java.time.Duration; import java.util.HashMap; import java.util.Map; -import static io.appium.java_client.MobileCommand.ACTIVATE_APP; -import static io.appium.java_client.MobileCommand.INSTALL_APP; -import static io.appium.java_client.MobileCommand.IS_APP_INSTALLED; -import static io.appium.java_client.MobileCommand.QUERY_APP_STATE; -import static io.appium.java_client.MobileCommand.REMOVE_APP; -import static io.appium.java_client.MobileCommand.RUN_APP_IN_BACKGROUND; -import static io.appium.java_client.MobileCommand.TERMINATE_APP; import static java.util.Objects.requireNonNull; import static java.util.Optional.ofNullable; @SuppressWarnings({"rawtypes", "unchecked"}) -public interface InteractsWithApps extends ExecutesMethod, CanRememberExtensionPresence { +public interface InteractsWithApps extends ExecutesMethod { /** * Install an app on the mobile device. @@ -61,19 +52,11 @@ default void installApp(String appPath) { */ default void installApp(String appPath, @Nullable BaseInstallApplicationOptions options) { final String extName = "mobile: installApp"; - try { - var args = new HashMap(); - args.put("app", appPath); - args.put("appPath", appPath); - ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); - } catch (UnsupportedCommandException | InvalidArgumentException e) { - // TODO: Remove the fallback - var args = new HashMap(); - args.put("appPath", appPath); - ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); - CommandExecutionHelper.execute(markExtensionAbsence(extName), Map.entry(INSTALL_APP, args)); - } + var args = new HashMap(); + args.put("app", appPath); + args.put("appPath", appPath); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + CommandExecutionHelper.executeScript(this, extName, args); } /** @@ -84,22 +67,12 @@ default void installApp(String appPath, @Nullable BaseInstallApplicationOptions */ default boolean isAppInstalled(String bundleId) { final String extName = "mobile: isAppInstalled"; - try { - return requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "bundleId", bundleId, - "appId", bundleId - )) - ); - } catch (UnsupportedCommandException | InvalidArgumentException e) { - // TODO: Remove the fallback - return requireNonNull( - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(IS_APP_INSTALLED, Map.of("bundleId", bundleId)) - ) - ); - } + return requireNonNull( + CommandExecutionHelper.executeScript(this, extName, Map.of( + "bundleId", bundleId, + "appId", bundleId + )) + ); } /** @@ -112,17 +85,9 @@ default boolean isAppInstalled(String bundleId) { */ default void runAppInBackground(Duration duration) { final String extName = "mobile: backgroundApp"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "seconds", duration.toMillis() / 1000.0 - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(RUN_APP_IN_BACKGROUND, Map.of("seconds", duration.toMillis() / 1000.0)) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "seconds", duration.toMillis() / 1000.0 + )); } /** @@ -145,27 +110,13 @@ default boolean removeApp(String bundleId) { */ default boolean removeApp(String bundleId, @Nullable BaseRemoveApplicationOptions options) { final String extName = "mobile: removeApp"; - try { - var args = new HashMap(); - args.put("bundleId", bundleId); - args.put("appId", bundleId); - ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); - return requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args) - ); - } catch (UnsupportedCommandException | InvalidArgumentException e) { - // TODO: Remove the fallback - var args = new HashMap(); - args.put("bundleId", bundleId); - ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); - //noinspection RedundantCast - return requireNonNull( - (Boolean) CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(REMOVE_APP, args) - ) - ); - } + var args = new HashMap(); + args.put("bundleId", bundleId); + args.put("appId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + return requireNonNull( + CommandExecutionHelper.executeScript(this, extName, args) + ); } /** @@ -188,19 +139,11 @@ default void activateApp(String bundleId) { */ default void activateApp(String bundleId, @Nullable BaseActivateApplicationOptions options) { final String extName = "mobile: activateApp"; - try { - var args = new HashMap(); - args.put("bundleId", bundleId); - args.put("appId", bundleId); - ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); - } catch (UnsupportedCommandException | InvalidArgumentException e) { - // TODO: Remove the fallback - var args = new HashMap(); - args.put("bundleId", bundleId); - ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); - CommandExecutionHelper.execute(markExtensionAbsence(extName), Map.entry(ACTIVATE_APP, args)); - } + var args = new HashMap(); + args.put("bundleId", bundleId); + args.put("appId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + CommandExecutionHelper.executeScript(this, extName, args); } /** @@ -211,29 +154,17 @@ default void activateApp(String bundleId, @Nullable BaseActivateApplicationOptio */ default ApplicationState queryAppState(String bundleId) { final String extName = "mobile: queryAppState"; - try { - return ApplicationState.ofCode( - requireNonNull( - CommandExecutionHelper.executeScript( - assertExtensionExists(extName), - extName, Map.of( - "bundleId", bundleId, - "appId", bundleId - ) - ) - ) - ); - } catch (UnsupportedCommandException | InvalidArgumentException e) { - // TODO: Remove the fallback - return ApplicationState.ofCode( - requireNonNull( - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(QUERY_APP_STATE, Map.of("bundleId", bundleId)) + return ApplicationState.ofCode( + requireNonNull( + CommandExecutionHelper.executeScript( + this, + extName, Map.of( + "bundleId", bundleId, + "appId", bundleId + ) ) - ) - ); - } + ) + ); } /** @@ -256,25 +187,12 @@ default boolean terminateApp(String bundleId) { */ default boolean terminateApp(String bundleId, @Nullable BaseTerminateApplicationOptions options) { final String extName = "mobile: terminateApp"; - try { - var args = new HashMap(); - args.put("bundleId", bundleId); - args.put("appId", bundleId); - ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); - return requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args) - ); - } catch (UnsupportedCommandException | InvalidArgumentException e) { - // TODO: Remove the fallback - var args = new HashMap(); - args.put("bundleId", bundleId); - ofNullable(options).map(BaseOptions::build).ifPresent(opts -> args.put("options", opts)); - //noinspection RedundantCast - return requireNonNull( - (Boolean) CommandExecutionHelper.execute( - markExtensionAbsence(extName), Map.entry(TERMINATE_APP, args) - ) - ); - } + var args = new HashMap(); + args.put("bundleId", bundleId); + args.put("appId", bundleId); + ofNullable(options).map(BaseOptions::build).ifPresent(args::putAll); + return requireNonNull( + CommandExecutionHelper.executeScript(this, extName, args) + ); } } diff --git a/src/main/java/io/appium/java_client/LocksDevice.java b/src/main/java/io/appium/java_client/LocksDevice.java index bd818b21b..04f8aa89b 100644 --- a/src/main/java/io/appium/java_client/LocksDevice.java +++ b/src/main/java/io/appium/java_client/LocksDevice.java @@ -16,17 +16,12 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - import java.time.Duration; import java.util.Map; -import static io.appium.java_client.MobileCommand.getIsDeviceLockedCommand; -import static io.appium.java_client.MobileCommand.lockDeviceCommand; -import static io.appium.java_client.MobileCommand.unlockDeviceCommand; import static java.util.Objects.requireNonNull; -public interface LocksDevice extends ExecutesMethod, CanRememberExtensionPresence { +public interface LocksDevice extends ExecutesMethod { /** * This method locks a device. It will return silently if the device @@ -46,14 +41,9 @@ default void lockDevice() { */ default void lockDevice(Duration duration) { final String extName = "mobile: lock"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "seconds", duration.getSeconds() - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), lockDeviceCommand(duration)); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "seconds", duration.getSeconds() + )); } /** @@ -62,16 +52,11 @@ default void lockDevice(Duration duration) { */ default void unlockDevice() { final String extName = "mobile: unlock"; - try { - //noinspection ConstantConditions - if (!(Boolean) CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: isLocked")) { - return; - } - CommandExecutionHelper.executeScript(this, extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), unlockDeviceCommand()); + //noinspection ConstantConditions + if (!(Boolean) CommandExecutionHelper.executeScript(this, "mobile: isLocked")) { + return; } + CommandExecutionHelper.executeScript(this, extName); } /** @@ -81,15 +66,8 @@ default void unlockDevice() { */ default boolean isDeviceLocked() { final String extName = "mobile: isLocked"; - try { - return requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName) - ); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return requireNonNull( - CommandExecutionHelper.execute(markExtensionAbsence(extName), getIsDeviceLockedCommand()) - ); - } + return requireNonNull( + CommandExecutionHelper.executeScript(this, extName) + ); } } diff --git a/src/main/java/io/appium/java_client/MobileCommand.java b/src/main/java/io/appium/java_client/MobileCommand.java index d35a26150..ff3cd76aa 100644 --- a/src/main/java/io/appium/java_client/MobileCommand.java +++ b/src/main/java/io/appium/java_client/MobileCommand.java @@ -24,150 +24,33 @@ import org.jspecify.annotations.Nullable; import java.nio.charset.StandardCharsets; -import java.time.Duration; import java.util.Collections; import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Optional; -import static io.appium.java_client.internal.Strings.isNullOrEmpty; - /** - * The repository of mobile commands defined in the Mobile JSON - * wire protocol. - * Most of these commands are platform-specific obsolete things and should eventually be replaced with - * calls to corresponding `mobile:` extensions, so we don't abuse non-w3c APIs + * The repository of the Appium commands that are not defined by the W3C protocol. + * Platform-specific features are available as `mobile:` extensions of the drivers. */ @SuppressWarnings({"checkstyle:HideUtilityClassConstructor", "checkstyle:ConstantName"}) public class MobileCommand { - //General - @Deprecated - protected static final String RESET; - @Deprecated - protected static final String GET_STRINGS; - @Deprecated - public static final String SET_VALUE; - @Deprecated - protected static final String PULL_FILE; - @Deprecated - protected static final String PULL_FOLDER; - @Deprecated - public static final String RUN_APP_IN_BACKGROUND; - @Deprecated - public static final String LAUNCH_APP; - @Deprecated - public static final String CLOSE_APP; - @Deprecated - protected static final String GET_DEVICE_TIME; @Deprecated protected static final String GET_SESSION; protected static final String LOG_EVENT; protected static final String GET_EVENTS; - //region Applications Management - @Deprecated - protected static final String IS_APP_INSTALLED; - @Deprecated - protected static final String INSTALL_APP; - @Deprecated - protected static final String ACTIVATE_APP; - @Deprecated - protected static final String QUERY_APP_STATE; - @Deprecated - protected static final String TERMINATE_APP; - @Deprecated - protected static final String REMOVE_APP; - //endregion - - //region Clipboard - @Deprecated - public static final String GET_CLIPBOARD; - @Deprecated - public static final String SET_CLIPBOARD; - //endregion + // The file transfer commands of the drivers that have no `mobile:` extensions for them, e.g. Windows + public static final String PULL_FILE; + public static final String PULL_FOLDER; + public static final String PUSH_FILE; - @Deprecated - protected static final String GET_PERFORMANCE_DATA; - @Deprecated - protected static final String GET_SUPPORTED_PERFORMANCE_DATA_TYPES; - - @Deprecated public static final String START_RECORDING_SCREEN; - @Deprecated public static final String STOP_RECORDING_SCREEN; - @Deprecated - protected static final String HIDE_KEYBOARD; - @Deprecated - protected static final String LOCK; - //iOS - @Deprecated - protected static final String SHAKE; - @Deprecated - protected static final String TOUCH_ID; - @Deprecated - protected static final String TOUCH_ID_ENROLLMENT; //Android - @Deprecated - public static final String CURRENT_ACTIVITY; - @Deprecated - protected static final String END_TEST_COVERAGE; - @Deprecated - protected static final String GET_DISPLAY_DENSITY; - @Deprecated - protected static final String GET_NETWORK_CONNECTION; - @Deprecated - protected static final String GET_SYSTEM_BARS; - @Deprecated - protected static final String IS_KEYBOARD_SHOWN; - @Deprecated - protected static final String IS_LOCKED; - @Deprecated - public static final String LONG_PRESS_KEY_CODE; - @Deprecated - protected static final String FINGER_PRINT; - @Deprecated - protected static final String OPEN_NOTIFICATIONS; - @Deprecated - public static final String PRESS_KEY_CODE; - @Deprecated - protected static final String PUSH_FILE; - @Deprecated - protected static final String SET_NETWORK_CONNECTION; - @Deprecated - protected static final String START_ACTIVITY; - @Deprecated - protected static final String TOGGLE_LOCATION_SERVICES; - @Deprecated - protected static final String UNLOCK; - @Deprecated - public static final String REPLACE_VALUE; protected static final String GET_SETTINGS; - @Deprecated protected static final String SET_SETTINGS; - @Deprecated - public static final String GET_CURRENT_PACKAGE; - @Deprecated - public static final String SEND_SMS; - @Deprecated - public static final String GSM_CALL; - @Deprecated - public static final String GSM_SIGNAL; - @Deprecated - public static final String GSM_VOICE; - @Deprecated - public static final String NETWORK_SPEED; - @Deprecated - public static final String POWER_CAPACITY; - @Deprecated - public static final String POWER_AC_STATE; - @Deprecated - protected static final String TOGGLE_WIFI; - @Deprecated - protected static final String TOGGLE_AIRPLANE_MODE; - @Deprecated - protected static final String TOGGLE_DATA; protected static final String COMPARE_IMAGES; protected static final String EXECUTE_DRIVER_SCRIPT; @Deprecated @@ -189,99 +72,31 @@ public class MobileCommand { public static final Map commandRepository; static { - RESET = "reset"; - GET_STRINGS = "getStrings"; - SET_VALUE = "setValue"; - PULL_FILE = "pullFile"; - PULL_FOLDER = "pullFolder"; - RUN_APP_IN_BACKGROUND = "runAppInBackground"; - LAUNCH_APP = "launchApp"; - CLOSE_APP = "closeApp"; - GET_DEVICE_TIME = "getDeviceTime"; GET_SESSION = "getSession"; LOG_EVENT = "logCustomEvent"; GET_EVENTS = "getLogEvents"; - //region Applications Management - IS_APP_INSTALLED = "isAppInstalled"; - QUERY_APP_STATE = "queryAppState"; - TERMINATE_APP = "terminateApp"; - ACTIVATE_APP = "activateApp"; - REMOVE_APP = "removeApp"; - INSTALL_APP = "installApp"; - //endregion - - //region Clipboard - SET_CLIPBOARD = "setClipboard"; - GET_CLIPBOARD = "getClipboard"; - //endregion - - GET_PERFORMANCE_DATA = "getPerformanceData"; - GET_SUPPORTED_PERFORMANCE_DATA_TYPES = "getSuppportedPerformanceDataTypes"; + PULL_FILE = "pullFile"; + PULL_FOLDER = "pullFolder"; + PUSH_FILE = "pushFile"; START_RECORDING_SCREEN = "startRecordingScreen"; STOP_RECORDING_SCREEN = "stopRecordingScreen"; - HIDE_KEYBOARD = "hideKeyboard"; - LOCK = "lock"; - SHAKE = "shake"; - TOUCH_ID = "touchId"; - TOUCH_ID_ENROLLMENT = "toggleEnrollTouchId"; - - CURRENT_ACTIVITY = "currentActivity"; - END_TEST_COVERAGE = "endTestCoverage"; - GET_DISPLAY_DENSITY = "getDisplayDensity"; - GET_NETWORK_CONNECTION = "getNetworkConnection"; - GET_SYSTEM_BARS = "getSystemBars"; - IS_KEYBOARD_SHOWN = "isKeyboardShown"; - IS_LOCKED = "isLocked"; - LONG_PRESS_KEY_CODE = "longPressKeyCode"; - FINGER_PRINT = "fingerPrint"; - OPEN_NOTIFICATIONS = "openNotifications"; - PRESS_KEY_CODE = "pressKeyCode"; - PUSH_FILE = "pushFile"; - SET_NETWORK_CONNECTION = "setNetworkConnection"; - START_ACTIVITY = "startActivity"; - TOGGLE_LOCATION_SERVICES = "toggleLocationServices"; - UNLOCK = "unlock"; - REPLACE_VALUE = "replaceValue"; GET_SETTINGS = "getSettings"; SET_SETTINGS = "setSettings"; - GET_CURRENT_PACKAGE = "getCurrentPackage"; - SEND_SMS = "sendSMS"; - GSM_CALL = "gsmCall"; - GSM_SIGNAL = "gsmSignal"; - GSM_VOICE = "gsmVoice"; - NETWORK_SPEED = "networkSpeed"; - POWER_CAPACITY = "powerCapacity"; - POWER_AC_STATE = "powerAC"; - TOGGLE_WIFI = "toggleWiFi"; - TOGGLE_AIRPLANE_MODE = "toggleFlightMode"; - TOGGLE_DATA = "toggleData"; COMPARE_IMAGES = "compareImages"; EXECUTE_DRIVER_SCRIPT = "executeDriverScript"; GET_ALLSESSION = "getAllSessions"; EXECUTE_GOOGLE_CDP_COMMAND = "executeCdp"; commandRepository = new HashMap<>(); - commandRepository.put(RESET, postC("/session/:sessionId/appium/app/reset")); - commandRepository.put(GET_STRINGS, postC("/session/:sessionId/appium/app/strings")); - commandRepository.put(SET_VALUE, postC("/session/:sessionId/appium/element/:id/value")); commandRepository.put(PULL_FILE, postC("/session/:sessionId/appium/device/pull_file")); commandRepository.put(PULL_FOLDER, postC("/session/:sessionId/appium/device/pull_folder")); - commandRepository.put(HIDE_KEYBOARD, postC("/session/:sessionId/appium/device/hide_keyboard")); - commandRepository.put(RUN_APP_IN_BACKGROUND, postC("/session/:sessionId/appium/app/background")); - commandRepository.put(LAUNCH_APP, postC("/session/:sessionId/appium/app/launch")); - commandRepository.put(CLOSE_APP, postC("/session/:sessionId/appium/app/close")); - commandRepository.put(LOCK, postC("/session/:sessionId/appium/device/lock")); + commandRepository.put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")); commandRepository.put(GET_SETTINGS, getC("/session/:sessionId/appium/settings")); commandRepository.put(SET_SETTINGS, postC("/session/:sessionId/appium/settings")); - commandRepository.put(GET_DEVICE_TIME, getC("/session/:sessionId/appium/device/system_time")); commandRepository.put(GET_SESSION, getC("/session/:sessionId/")); - commandRepository.put(GET_SUPPORTED_PERFORMANCE_DATA_TYPES, - postC("/session/:sessionId/appium/performanceData/types")); - commandRepository.put(GET_PERFORMANCE_DATA, - postC("/session/:sessionId/appium/getPerformanceData")); commandRepository.put(START_RECORDING_SCREEN, postC("/session/:sessionId/appium/start_recording_screen")); commandRepository.put(STOP_RECORDING_SCREEN, @@ -291,62 +106,7 @@ public class MobileCommand { commandRepository.put(LOG_EVENT, postC("/session/:sessionId/appium/log_event")); - //region Applications Management - commandRepository.put(IS_APP_INSTALLED, postC("/session/:sessionId/appium/device/app_installed")); - commandRepository.put(INSTALL_APP, postC("/session/:sessionId/appium/device/install_app")); - commandRepository.put(ACTIVATE_APP, postC("/session/:sessionId/appium/device/activate_app")); - commandRepository.put(REMOVE_APP, postC("/session/:sessionId/appium/device/remove_app")); - commandRepository.put(TERMINATE_APP, postC("/session/:sessionId/appium/device/terminate_app")); - commandRepository.put(QUERY_APP_STATE, postC("/session/:sessionId/appium/device/app_state")); - //endregion - - //region Clipboard - commandRepository.put(GET_CLIPBOARD, postC("/session/:sessionId/appium/device/get_clipboard")); - commandRepository.put(SET_CLIPBOARD, postC("/session/:sessionId/appium/device/set_clipboard")); - //endregion - - //iOS - commandRepository.put(SHAKE, postC("/session/:sessionId/appium/device/shake")); - commandRepository.put(TOUCH_ID, postC("/session/:sessionId/appium/simulator/touch_id")); - commandRepository.put(TOUCH_ID_ENROLLMENT, - postC("/session/:sessionId/appium/simulator/toggle_touch_id_enrollment")); //Android - commandRepository.put(CURRENT_ACTIVITY, - getC("/session/:sessionId/appium/device/current_activity")); - commandRepository.put(END_TEST_COVERAGE, - postC("/session/:sessionId/appium/app/end_test_coverage")); - commandRepository.put(GET_DISPLAY_DENSITY, getC("/session/:sessionId/appium/device/display_density")); - commandRepository.put(GET_NETWORK_CONNECTION, getC("/session/:sessionId/network_connection")); - commandRepository.put(GET_SYSTEM_BARS, getC("/session/:sessionId/appium/device/system_bars")); - commandRepository.put(IS_KEYBOARD_SHOWN, getC("/session/:sessionId/appium/device/is_keyboard_shown")); - commandRepository.put(IS_LOCKED, postC("/session/:sessionId/appium/device/is_locked")); - commandRepository.put(LONG_PRESS_KEY_CODE, - postC("/session/:sessionId/appium/device/long_press_keycode")); - commandRepository.put(FINGER_PRINT, postC("/session/:sessionId/appium/device/finger_print")); - commandRepository.put(OPEN_NOTIFICATIONS, - postC("/session/:sessionId/appium/device/open_notifications")); - commandRepository.put(PRESS_KEY_CODE, - postC("/session/:sessionId/appium/device/press_keycode")); - commandRepository.put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")); - commandRepository.put(SET_NETWORK_CONNECTION, - postC("/session/:sessionId/network_connection")); - commandRepository.put(START_ACTIVITY, - postC("/session/:sessionId/appium/device/start_activity")); - commandRepository.put(TOGGLE_LOCATION_SERVICES, - postC("/session/:sessionId/appium/device/toggle_location_services")); - commandRepository.put(UNLOCK, postC("/session/:sessionId/appium/device/unlock")); - commandRepository.put(REPLACE_VALUE, postC("/session/:sessionId/appium/element/:id/replace_value")); - commandRepository.put(GET_CURRENT_PACKAGE, getC("/session/:sessionId/appium/device/current_package")); - commandRepository.put(SEND_SMS, postC("/session/:sessionId/appium/device/send_sms")); - commandRepository.put(GSM_CALL, postC("/session/:sessionId/appium/device/gsm_call")); - commandRepository.put(GSM_SIGNAL, postC("/session/:sessionId/appium/device/gsm_signal")); - commandRepository.put(GSM_VOICE, postC("/session/:sessionId/appium/device/gsm_voice")); - commandRepository.put(NETWORK_SPEED, postC("/session/:sessionId/appium/device/network_speed")); - commandRepository.put(POWER_CAPACITY, postC("/session/:sessionId/appium/device/power_capacity")); - commandRepository.put(POWER_AC_STATE, postC("/session/:sessionId/appium/device/power_ac")); - commandRepository.put(TOGGLE_WIFI, postC("/session/:sessionId/appium/device/toggle_wifi")); - commandRepository.put(TOGGLE_AIRPLANE_MODE, postC("/session/:sessionId/appium/device/toggle_airplane_mode")); - commandRepository.put(TOGGLE_DATA, postC("/session/:sessionId/appium/device/toggle_data")); commandRepository.put(COMPARE_IMAGES, postC("/session/:sessionId/appium/compare_images")); commandRepository.put(EXECUTE_DRIVER_SCRIPT, postC("/session/:sessionId/appium/execute_driver")); commandRepository.put(GET_ALLSESSION, getC("/sessions")); @@ -395,161 +155,6 @@ public static AppiumCommandInfo deleteC(String url) { return new AppiumCommandInfo(url, HttpMethod.DELETE); } - /** - * This method forms a {@link Map} of parameters for the keyboard hiding. - * - * @param keyName The button pressed by the mobile driver to attempt hiding the - * keyboard. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> hideKeyboardCommand(String keyName) { - return Map.entry(HIDE_KEYBOARD, Map.of("keyName", keyName)); - } - - /** - * This method forms a {@link Map} of parameters for the keyboard hiding. - * - * @param strategy HideKeyboardStrategy. - * @param keyName a String, representing the text displayed on the button of the - * keyboard you want to press. For example: "Done". - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> hideKeyboardCommand(String strategy, - String keyName) { - return Map.entry(HIDE_KEYBOARD, Map.of( - "strategy", strategy, - "key", keyName - )); - } - - /** - * Prepares single argument. - * - * @param param is a parameter name. - * @param value is the parameter value. - * @return built {@link Map}. - * @deprecated Use {@link Map#of(Object, Object)} - */ - @Deprecated - public static Map prepareArguments(String param, Object value) { - return Map.of(param, value); - } - - /** - * Prepares collection of arguments. - * - * @param params is the array with parameter names. - * @param values is the array with parameter values. - * @return built {@link Map}. - * @deprecated Use {@link Map#of(Object, Object, Object, Object)} - */ - @Deprecated - public static Map prepareArguments(String[] params, Object[] values) { - Map result = new LinkedHashMap<>(); - for (int i = 0; i < params.length; i++) { - if (!isNullOrEmpty(params[i]) && values[i] != null) { - result.put(params[i], values[i]); - } - } - return Collections.unmodifiableMap(result); - } - - /** - * This method forms a {@link Map} of parameters for the key event invocation. - * - * @param key code for the key pressed on the device. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> pressKeyCodeCommand(int key) { - return Map.entry(PRESS_KEY_CODE, Map.of("keycode", key)); - } - - /** - * This method forms a {@link Map} of parameters for the key event invocation. - * - * @param key code for the key pressed on the Android device. - * @param metastate metastate for the keypress. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> pressKeyCodeCommand(int key, - Integer metastate) { - return Map.entry(PRESS_KEY_CODE, Map.of( - "keycode", key, - "metastate", metastate - )); - } - - /** - * This method forms a {@link Map} of parameters for the long key event invocation. - * - * @param key code for the long key pressed on the device. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> longPressKeyCodeCommand(int key) { - return Map.entry(LONG_PRESS_KEY_CODE, Map.of("keycode", key)); - } - - /** - * This method forms a {@link Map} of parameters for the long key event invocation. - * - * @param key code for the long key pressed on the Android device. - * @param metastate metastate for the long key press. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> longPressKeyCodeCommand(int key, - Integer metastate) { - return Map.entry(LONG_PRESS_KEY_CODE, Map.of( - "keycode", key, - "metastate", metastate - )); - } - - /** - * This method forms a {@link Map} of parameters for the device locking. - * - * @param duration for how long to lock the screen for. Minimum time resolution is one second - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> lockDeviceCommand(Duration duration) { - return Map.entry(LOCK, Map.of("seconds", duration.getSeconds())); - } - - /** - * This method forms a {@link Map} of parameters for the device unlocking. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> unlockDeviceCommand() { - return Map.entry(UNLOCK, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the device locked query. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> getIsDeviceLockedCommand() { - return Map.entry(IS_LOCKED, Map.of()); - } - public static Map.Entry> getSettingsCommand() { return Map.entry(GET_SETTINGS, Map.of()); } @@ -562,22 +167,6 @@ public static Map prepareArguments(String[] params, Object[] val return Map.entry(SET_SETTINGS, Map.of("settings", settings)); } - /** - * This method forms a {@link Map} of parameters for the file pushing. - * - * @param remotePath Path to file to write data to on remote device - * @param base64Data Base64 encoded byte array of data to write to remote device - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> pushFileCommand(String remotePath, byte[] base64Data) { - return Map.entry(PUSH_FILE, Map.of( - "path", remotePath, - "data", new String(base64Data, StandardCharsets.UTF_8) - )); - } - public static Map.Entry> startRecordingScreenCommand(BaseStartScreenRecordingOptions opts) { return Map.entry(START_RECORDING_SCREEN, Map.of("options", opts.build())); } @@ -606,14 +195,4 @@ public static Map prepareArguments(String[] params, Object[] val return Map.entry(COMPARE_IMAGES, Collections.unmodifiableMap(args)); } - /** - * This method forms a {@link Map} of parameters for the checking of the keyboard state (is it shown or not). - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated This helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> isKeyboardShownCommand() { - return Map.entry(IS_KEYBOARD_SHOWN, Map.of()); - } } diff --git a/src/main/java/io/appium/java_client/PullsFiles.java b/src/main/java/io/appium/java_client/PullsFiles.java index 3c1e7ccff..00552371d 100644 --- a/src/main/java/io/appium/java_client/PullsFiles.java +++ b/src/main/java/io/appium/java_client/PullsFiles.java @@ -16,17 +16,13 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Map; -import static io.appium.java_client.MobileCommand.PULL_FILE; -import static io.appium.java_client.MobileCommand.PULL_FOLDER; import static java.util.Objects.requireNonNull; -public interface PullsFiles extends ExecutesMethod, CanRememberExtensionPresence { +public interface PullsFiles extends ExecutesMethod { /** * Pull a file from the remote system. @@ -43,20 +39,11 @@ public interface PullsFiles extends ExecutesMethod, CanRememberExtensionPresence default byte[] pullFile(String remotePath) { final String extName = "mobile: pullFile"; String base64String; - try { - base64String = requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, - Map.of("remotePath", remotePath) - ) - ); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - base64String = requireNonNull( - CommandExecutionHelper.execute(markExtensionAbsence(extName), - Map.entry(PULL_FILE, Map.of("path", remotePath)) - ) - ); - } + base64String = requireNonNull( + CommandExecutionHelper.executeScript(this, extName, + Map.of("remotePath", remotePath) + ) + ); return Base64.getDecoder().decode(base64String.getBytes(StandardCharsets.UTF_8)); } @@ -75,20 +62,11 @@ default byte[] pullFile(String remotePath) { default byte[] pullFolder(String remotePath) { final String extName = "mobile: pullFolder"; String base64String; - try { - base64String = requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, - Map.of("remotePath", remotePath) - ) - ); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - base64String = requireNonNull( - CommandExecutionHelper.execute(markExtensionAbsence(extName), - Map.entry(PULL_FOLDER, Map.of("path", remotePath)) - ) - ); - } + base64String = requireNonNull( + CommandExecutionHelper.executeScript(this, extName, + Map.of("remotePath", remotePath) + ) + ); return Base64.getDecoder().decode(base64String.getBytes(StandardCharsets.UTF_8)); } diff --git a/src/main/java/io/appium/java_client/PushesFiles.java b/src/main/java/io/appium/java_client/PushesFiles.java index a30bf0d3b..49b1592cf 100644 --- a/src/main/java/io/appium/java_client/PushesFiles.java +++ b/src/main/java/io/appium/java_client/PushesFiles.java @@ -16,8 +16,6 @@ package io.appium.java_client; -import org.openqa.selenium.UnsupportedCommandException; - import java.io.File; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -25,9 +23,7 @@ import java.util.Base64; import java.util.Map; -import static io.appium.java_client.MobileCommand.pushFileCommand; - -public interface PushesFiles extends ExecutesMethod, CanRememberExtensionPresence { +public interface PushesFiles extends ExecutesMethod { /** * Saves base64-encoded data as a file on the remote system. @@ -40,15 +36,10 @@ public interface PushesFiles extends ExecutesMethod, CanRememberExtensionPresenc */ default void pushFile(String remotePath, byte[] base64Data) { final String extName = "mobile: pushFile"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "remotePath", remotePath, - "payload", new String(base64Data, StandardCharsets.UTF_8) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), pushFileCommand(remotePath, base64Data)); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "remotePath", remotePath, + "payload", new String(base64Data, StandardCharsets.UTF_8) + )); } /** diff --git a/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java b/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java deleted file mode 100644 index 1b74ea4e4..000000000 --- a/src/main/java/io/appium/java_client/android/AndroidMobileCommandHelper.java +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * 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 io.appium.java_client.android; - -import io.appium.java_client.MobileCommand; -import io.appium.java_client.remote.AppiumWebElement; - -import java.util.Map; - -import static java.util.Locale.ROOT; - -/** - * This util class helps to prepare parameters of Android-specific JSONWP - * commands. - */ -public class AndroidMobileCommandHelper extends MobileCommand { - - /** - * This method forms a {@link Map} of parameters for the getting of the current activity. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> currentActivityCommand() { - return Map.entry(CURRENT_ACTIVITY, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the getting of the current package. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> currentPackageCommand() { - return Map.entry(GET_CURRENT_PACKAGE, Map.of()); - } - - /** - * returns the information type of the system state which is supported to read - * as like cpu, memory, network traffic, and battery. - * @return output - array like below - * [cpuinfo, batteryinfo, networkinfo, memoryinfo] - * - */ - public static Map.Entry> getSupportedPerformanceDataTypesCommand() { - return Map.entry(GET_SUPPORTED_PERFORMANCE_DATA_TYPES, Map.of()); - } - - /** - * returns the resource usage information of the application. the resource is one of the system state - * which means cpu, memory, network traffic, and battery. - * - * @param packageName the package name of the application - * @param dataType the type of system state which wants to read. - * It should be one of the supported performance data types, - * the return value of the function "getSupportedPerformanceDataTypes" - * @param dataReadTimeout the number of attempts to read - * @return table of the performance data, The first line of the table represents the type of data. - * The remaining lines represent the values of the data. - * in case of battery info : [[power], [23]] - * in case of memory info : - * [[totalPrivateDirty, nativePrivateDirty, dalvikPrivateDirty, eglPrivateDirty, glPrivateDirty, - * totalPss, nativePss, dalvikPss, eglPss, glPss, nativeHeapAllocatedSize, nativeHeapSize], - * [18360, 8296, 6132, null, null, 42588, 8406, 7024, null, null, 26519, 10344]] - * in case of network info : - * [[bucketStart, activeTime, rxBytes, rxPackets, txBytes, txPackets, operations, bucketDuration,], - * [1478091600000, null, 1099075, 610947, 928, 114362, 769, 0, 3600000], - * [1478095200000, null, 1306300, 405997, 509, 46359, 370, 0, 3600000]] - * in case of network info : - * [[st, activeTime, rb, rp, tb, tp, op, bucketDuration], - * [1478088000, null, null, 32115296, 34291, 2956805, 25705, 0, 3600], - * [1478091600, null, null, 2714683, 11821, 1420564, 12650, 0, 3600], - * [1478095200, null, null, 10079213, 19962, 2487705, 20015, 0, 3600], - * [1478098800, null, null, 4444433, 10227, 1430356, 10493, 0, 3600]] - * in case of cpu info : [[user, kernel], [0.9, 1.3]] - */ - public static Map.Entry> getPerformanceDataCommand( - String packageName, String dataType, int dataReadTimeout) { - return Map.entry(GET_PERFORMANCE_DATA, Map.of( - "packageName", packageName, - "dataType", dataType, - "dataReadTimeout", dataReadTimeout - )); - } - - /** - * This method forms a {@link Map} of parameters to retrieve the display density of the Android device. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> getDisplayDensityCommand() { - return Map.entry(GET_DISPLAY_DENSITY, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the getting of a network connection value. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> getNetworkConnectionCommand() { - return Map.entry(GET_NETWORK_CONNECTION, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters to retrieve visibility and bounds information of the status and - * navigation bars. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> getSystemBarsCommand() { - return Map.entry(GET_SYSTEM_BARS, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the finger print authentication invocation. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> isLockedCommand() { - return Map.entry(IS_LOCKED, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the finger print authentication invocation. - * - * @param fingerPrintId enrolled virtual fingerprint id passed to - * {@code adb emu finger touch}, not the emulator UI Finger 1-10 labels - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> fingerPrintCommand(int fingerPrintId) { - return Map.entry(FINGER_PRINT, Map.of("fingerprintId", fingerPrintId)); - } - - /** - * This method forms a {@link Map} of parameters for the notification opening. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> openNotificationsCommand() { - return Map.entry(OPEN_NOTIFICATIONS, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the setting of device network connection. - * - * @param bitMask The bitmask of the desired connection - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> setConnectionCommand(long bitMask) { - return Map.entry(SET_NETWORK_CONNECTION, Map.of( - "name", "network_connection", - "parameters", Map.of("type", bitMask) - )); - } - - /** - * This method forms a {@link Map} of parameters for the toggling of location services. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> toggleLocationServicesCommand() { - return Map.entry(TOGGLE_LOCATION_SERVICES, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the element. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> unlockCommand() { - return Map.entry(UNLOCK, Map.of()); - } - - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param remoteWebElement an instance which contains an element ID - * @param value a new value - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> replaceElementValueCommand( - AppiumWebElement remoteWebElement, String value) { - return Map.entry(REPLACE_VALUE, Map.of( - "id", remoteWebElement.getId(), - "value", value - )); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param phoneNumber The phone number of message sender - * @param message The message content - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> sendSMSCommand( - String phoneNumber, String message) { - return Map.entry(SEND_SMS, Map.of( - "phoneNumber", phoneNumber, - "message", message - )); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param phoneNumber The phone number of message sender - * @param gsmCallActions One of available GSM call actions - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> gsmCallCommand( - String phoneNumber, GsmCallActions gsmCallActions) { - return Map.entry(GSM_CALL, Map.of( - "phoneNumber", phoneNumber, - "action", gsmCallActions.name().toLowerCase(ROOT) - )); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param gsmSignalStrength One of available GSM signal strength - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> gsmSignalStrengthCommand( - GsmSignalStrength gsmSignalStrength) { - return Map.entry(GSM_SIGNAL, - Map.of( - // https://github.com/appium/appium/issues/12234 - "signalStrengh", gsmSignalStrength.ordinal(), - "signalStrength", gsmSignalStrength.ordinal() - )); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param gsmVoiceState One of available GSM voice state - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> gsmVoiceCommand( - GsmVoiceState gsmVoiceState) { - return Map.entry(GSM_VOICE, Map.of("state", gsmVoiceState.name().toLowerCase(ROOT))); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param networkSpeed One of possible NETWORK_SPEED values - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> networkSpeedCommand( - NetworkSpeed networkSpeed) { - return Map.entry(NETWORK_SPEED, Map.of("netspeed", networkSpeed.name().toLowerCase(ROOT))); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param percent A number in range [0, 4] - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> powerCapacityCommand( - int percent) { - return Map.entry(POWER_CAPACITY, Map.of("percent", percent)); - } - - /** - * This method forms a {@link Map} of parameters for the element - * value replacement. It is used against input elements - * - * @param powerACState One of available power AC state - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> powerACCommand( - PowerACState powerACState) { - return Map.entry(POWER_AC_STATE, Map.of("state", powerACState.name().toLowerCase(ROOT))); - } - - /** - * This method forms a {@link Map} of parameters for the toggling of wifi. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> toggleWifiCommand() { - return Map.entry(TOGGLE_WIFI, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the toggle airplane mode. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> toggleAirplaneCommand() { - return Map.entry(TOGGLE_AIRPLANE_MODE, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the toggle data. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - */ - @Deprecated - public static Map.Entry> toggleDataCommand() { - return Map.entry(TOGGLE_DATA, Map.of()); - } -} diff --git a/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java b/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java index f5461253e..ac2209a13 100644 --- a/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java +++ b/src/main/java/io/appium/java_client/android/AuthenticatesByFinger.java @@ -1,15 +1,11 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -import static io.appium.java_client.android.AndroidMobileCommandHelper.fingerPrintCommand; - -public interface AuthenticatesByFinger extends ExecutesMethod, CanRememberExtensionPresence { +public interface AuthenticatesByFinger extends ExecutesMethod { /** * Authenticate users by using their finger print scans on supported emulators. @@ -19,13 +15,8 @@ public interface AuthenticatesByFinger extends ExecutesMethod, CanRememberExtens */ default void fingerPrint(int fingerPrintId) { final String extName = "mobile: fingerprint"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "fingerprintId", fingerPrintId - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), fingerPrintCommand(fingerPrintId)); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "fingerprintId", fingerPrintId + )); } } diff --git a/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java b/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java index 161ae5063..632a8e511 100644 --- a/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java +++ b/src/main/java/io/appium/java_client/android/CanReplaceElementValue.java @@ -1,15 +1,12 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import io.appium.java_client.MobileCommand; import io.appium.java_client.remote.AppiumWebElement; -import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -public interface CanReplaceElementValue extends ExecutesMethod, CanRememberExtensionPresence { +public interface CanReplaceElementValue extends ExecutesMethod { /** * Sends a text to the given element by replacing its previous content. * @@ -22,21 +19,9 @@ public interface CanReplaceElementValue extends ExecutesMethod, CanRememberExten */ default void replaceElementValue(AppiumWebElement element, String value) { final String extName = "mobile: replaceElementValue"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "elementId", element.getId(), - "text", value - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(MobileCommand.REPLACE_VALUE, Map.of( - "id", element.getId(), - "text", value, - "value", value - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "elementId", element.getId(), + "text", value + )); } } diff --git a/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java index 8b7018e76..5f173c120 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidClipboard.java @@ -24,7 +24,6 @@ import java.util.Base64; import java.util.Map; -import static io.appium.java_client.MobileCommand.SET_CLIPBOARD; import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; @@ -37,12 +36,10 @@ public interface HasAndroidClipboard extends HasClipboard { * @param base64Content base64-encoded content to be set. */ default void setClipboard(String label, ClipboardContentType contentType, byte[] base64Content) { - CommandExecutionHelper.execute(this, Map.entry(SET_CLIPBOARD, - Map.of( - "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), - "contentType", contentType.name().toLowerCase(ROOT), - "label", requireNonNull(label) - ) + CommandExecutionHelper.executeScript(this, "mobile: setClipboard", Map.of( + "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), + "contentType", contentType.name().toLowerCase(ROOT), + "label", requireNonNull(label) )); } diff --git a/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java b/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java index 3e230b3a3..76c596b1a 100644 --- a/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java +++ b/src/main/java/io/appium/java_client/android/HasAndroidDeviceDetails.java @@ -1,16 +1,11 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getDisplayDensityCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getSystemBarsCommand; - -public interface HasAndroidDeviceDetails extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasAndroidDeviceDetails extends ExecutesMethod { /** Retrieve the display density of the Android device. @@ -19,12 +14,7 @@ public interface HasAndroidDeviceDetails extends ExecutesMethod, CanRememberExte */ default Long getDisplayDensity() { final String extName = "mobile: getDisplayDensity"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute(markExtensionAbsence(extName), getDisplayDensityCommand()); - } + return CommandExecutionHelper.executeScript(this, extName); } /** @@ -34,12 +24,7 @@ default Long getDisplayDensity() { */ default Map> getSystemBars() { final String extName = "mobile: getSystemBars"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute(markExtensionAbsence(extName), getSystemBarsCommand()); - } + return CommandExecutionHelper.executeScript(this, extName); } } diff --git a/src/main/java/io/appium/java_client/android/HasNotifications.java b/src/main/java/io/appium/java_client/android/HasNotifications.java index 0b7f7365b..0911095be 100644 --- a/src/main/java/io/appium/java_client/android/HasNotifications.java +++ b/src/main/java/io/appium/java_client/android/HasNotifications.java @@ -1,24 +1,15 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; -import static io.appium.java_client.android.AndroidMobileCommandHelper.openNotificationsCommand; - -public interface HasNotifications extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasNotifications extends ExecutesMethod { /** * Opens notification drawer on the device under test. */ default void openNotifications() { final String extName = "mobile: openNotifications"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), openNotificationsCommand()); - } + CommandExecutionHelper.executeScript(this, extName); } } diff --git a/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java b/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java index 9a175d14c..bca5a4e29 100644 --- a/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java +++ b/src/main/java/io/appium/java_client/android/HasSupportedPerformanceDataType.java @@ -1,17 +1,12 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.List; import java.util.Map; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getPerformanceDataCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getSupportedPerformanceDataTypesCommand; - -public interface HasSupportedPerformanceDataType extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasSupportedPerformanceDataType extends ExecutesMethod { /** * returns the information type of the system state which is supported to read @@ -22,14 +17,7 @@ public interface HasSupportedPerformanceDataType extends ExecutesMethod, CanReme */ default List getSupportedPerformanceDataTypes() { final String extName = "mobile: getPerformanceDataTypes"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute( - markExtensionAbsence(extName), getSupportedPerformanceDataTypesCommand() - ); - } + return CommandExecutionHelper.executeScript(this, extName); } /** @@ -62,16 +50,9 @@ default List getSupportedPerformanceDataTypes() { */ default List> getPerformanceData(String packageName, String dataType, int dataReadTimeout) { final String extName = "mobile: getPerformanceData"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "packageName", packageName, - "dataType", dataType - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute( - markExtensionAbsence(extName), getPerformanceDataCommand(packageName, dataType, dataReadTimeout) - ); - } + return CommandExecutionHelper.executeScript(this, extName, Map.of( + "packageName", packageName, + "dataType", dataType + )); } } diff --git a/src/main/java/io/appium/java_client/android/StartsActivity.java b/src/main/java/io/appium/java_client/android/StartsActivity.java index 23b0ad7a9..a49441b94 100644 --- a/src/main/java/io/appium/java_client/android/StartsActivity.java +++ b/src/main/java/io/appium/java_client/android/StartsActivity.java @@ -16,18 +16,11 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; import org.jspecify.annotations.Nullable; -import org.openqa.selenium.UnsupportedCommandException; -import java.util.Map; - -import static io.appium.java_client.MobileCommand.CURRENT_ACTIVITY; -import static io.appium.java_client.MobileCommand.GET_CURRENT_PACKAGE; - -public interface StartsActivity extends ExecutesMethod, CanRememberExtensionPresence { +public interface StartsActivity extends ExecutesMethod { /** * Get the current activity being run on the mobile device. * @@ -36,15 +29,7 @@ public interface StartsActivity extends ExecutesMethod, CanRememberExtensionPres @Nullable default String currentActivity() { final String extName = "mobile: getCurrentActivity"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(CURRENT_ACTIVITY, Map.of()) - ); - } + return CommandExecutionHelper.executeScript(this, extName); } /** @@ -55,14 +40,6 @@ default String currentActivity() { @Nullable default String getCurrentPackage() { final String extName = "mobile: getCurrentPackage"; - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(GET_CURRENT_PACKAGE, Map.of()) - ); - } + return CommandExecutionHelper.executeScript(this, extName); } } diff --git a/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java b/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java index 5c16bb293..c300bc0a2 100644 --- a/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java +++ b/src/main/java/io/appium/java_client/android/SupportsGpsStateManagement.java @@ -1,14 +1,11 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; -import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleLocationServicesCommand; import static java.util.Objects.requireNonNull; -public interface SupportsGpsStateManagement extends ExecutesMethod, CanRememberExtensionPresence { +public interface SupportsGpsStateManagement extends ExecutesMethod { /** * Toggles GPS service state. @@ -16,12 +13,7 @@ public interface SupportsGpsStateManagement extends ExecutesMethod, CanRememberE */ default void toggleLocationServices() { final String extName = "mobile: toggleGps"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleLocationServicesCommand()); - } + CommandExecutionHelper.executeScript(this, extName); } /** diff --git a/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java b/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java index 2992e5847..86c4b4a7f 100644 --- a/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java +++ b/src/main/java/io/appium/java_client/android/SupportsNetworkStateManagement.java @@ -1,35 +1,25 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleAirplaneCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleDataCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.toggleWifiCommand; import static java.util.Objects.requireNonNull; -public interface SupportsNetworkStateManagement extends ExecutesMethod, CanRememberExtensionPresence { +public interface SupportsNetworkStateManagement extends ExecutesMethod { /** * Toggles Wifi on and off. */ default void toggleWifi() { final String extName = "mobile: setConnectivity"; - try { - Map result = requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: getConnectivity") - ); - CommandExecutionHelper.executeScript(this, extName, Map.of( - "wifi", !((Boolean) result.get("wifi")) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleWifiCommand()); - } + Map result = requireNonNull( + CommandExecutionHelper.executeScript(this, "mobile: getConnectivity") + ); + CommandExecutionHelper.executeScript(this, extName, Map.of( + "wifi", !((Boolean) result.get("wifi")) + )); } /** @@ -38,17 +28,12 @@ default void toggleWifi() { */ default void toggleAirplaneMode() { final String extName = "mobile: setConnectivity"; - try { - Map result = requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: getConnectivity") - ); - CommandExecutionHelper.executeScript(this, extName, Map.of( - "airplaneMode", !((Boolean) result.get("airplaneMode")) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleAirplaneCommand()); - } + Map result = requireNonNull( + CommandExecutionHelper.executeScript(this, "mobile: getConnectivity") + ); + CommandExecutionHelper.executeScript(this, extName, Map.of( + "airplaneMode", !((Boolean) result.get("airplaneMode")) + )); } /** @@ -57,16 +42,11 @@ default void toggleAirplaneMode() { */ default void toggleData() { final String extName = "mobile: setConnectivity"; - try { - Map result = requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), "mobile: getConnectivity") - ); - CommandExecutionHelper.executeScript(this, extName, Map.of( - "data", !((Boolean) result.get("data")) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), toggleDataCommand()); - } + Map result = requireNonNull( + CommandExecutionHelper.executeScript(this, "mobile: getConnectivity") + ); + CommandExecutionHelper.executeScript(this, extName, Map.of( + "data", !((Boolean) result.get("data")) + )); } } diff --git a/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java b/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java index bb618b8cd..e678fedd9 100644 --- a/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java +++ b/src/main/java/io/appium/java_client/android/SupportsSpecialEmulatorCommands.java @@ -1,22 +1,13 @@ package io.appium.java_client.android; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -import static io.appium.java_client.MobileCommand.GSM_CALL; -import static io.appium.java_client.MobileCommand.GSM_SIGNAL; -import static io.appium.java_client.MobileCommand.GSM_VOICE; -import static io.appium.java_client.MobileCommand.NETWORK_SPEED; -import static io.appium.java_client.MobileCommand.POWER_AC_STATE; -import static io.appium.java_client.MobileCommand.POWER_CAPACITY; -import static io.appium.java_client.MobileCommand.SEND_SMS; import static java.util.Locale.ROOT; -public interface SupportsSpecialEmulatorCommands extends ExecutesMethod, CanRememberExtensionPresence { +public interface SupportsSpecialEmulatorCommands extends ExecutesMethod { /** * Emulate send SMS event on the connected emulator. @@ -26,21 +17,10 @@ public interface SupportsSpecialEmulatorCommands extends ExecutesMethod, CanReme */ default void sendSMS(String phoneNumber, String message) { final String extName = "mobile: sendSms"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "phoneNumber", phoneNumber, - "message", message - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(SEND_SMS, Map.of( - "phoneNumber", phoneNumber, - "message", message - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "phoneNumber", phoneNumber, + "message", message + )); } /** @@ -51,21 +31,10 @@ default void sendSMS(String phoneNumber, String message) { */ default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallAction) { final String extName = "mobile: gsmCall"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "phoneNumber", phoneNumber, - "action", gsmCallAction.toString().toLowerCase(ROOT) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(GSM_CALL, Map.of( - "phoneNumber", phoneNumber, - "action", gsmCallAction.toString().toLowerCase(ROOT) - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "phoneNumber", phoneNumber, + "action", gsmCallAction.toString().toLowerCase(ROOT) + )); } /** @@ -75,20 +44,9 @@ default void makeGsmCall(String phoneNumber, GsmCallActions gsmCallAction) { */ default void setGsmSignalStrength(GsmSignalStrength gsmSignalStrength) { final String extName = "mobile: gsmSignal"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "strength", gsmSignalStrength.ordinal() - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(GSM_SIGNAL, Map.of( - "signalStrengh", gsmSignalStrength.ordinal(), - "signalStrength", gsmSignalStrength.ordinal() - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "strength", gsmSignalStrength.ordinal() + )); } /** @@ -98,19 +56,9 @@ default void setGsmSignalStrength(GsmSignalStrength gsmSignalStrength) { */ default void setGsmVoice(GsmVoiceState gsmVoiceState) { final String extName = "mobile: gsmVoice"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "state", gsmVoiceState.toString().toLowerCase(ROOT) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(GSM_VOICE, Map.of( - "state", gsmVoiceState.name().toLowerCase(ROOT) - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "state", gsmVoiceState.toString().toLowerCase(ROOT) + )); } /** @@ -120,19 +68,9 @@ default void setGsmVoice(GsmVoiceState gsmVoiceState) { */ default void setNetworkSpeed(NetworkSpeed networkSpeed) { final String extName = "mobile: networkSpeed"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "speed", networkSpeed.toString().toLowerCase(ROOT) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(NETWORK_SPEED, Map.of( - "netspeed", networkSpeed.name().toLowerCase(ROOT) - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "speed", networkSpeed.toString().toLowerCase(ROOT) + )); } /** @@ -142,19 +80,9 @@ default void setNetworkSpeed(NetworkSpeed networkSpeed) { */ default void setPowerCapacity(int percent) { final String extName = "mobile: powerCapacity"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "percent", percent - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(POWER_CAPACITY, Map.of( - "percent", percent - )) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "percent", percent + )); } /** @@ -163,20 +91,10 @@ default void setPowerCapacity(int percent) { * @param powerACState One of available {@link PowerACState} values. */ default void setPowerAC(PowerACState powerACState) { - final String extName = "mobile: powerAC"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "state", powerACState.toString().toLowerCase(ROOT) - )); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(POWER_AC_STATE, Map.of( - "state", powerACState.name().toLowerCase(ROOT) - )) - ); - } + final String extName = "mobile: powerAc"; + CommandExecutionHelper.executeScript(this, extName, Map.of( + "state", powerACState.toString().toLowerCase(ROOT) + )); } } diff --git a/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java b/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java index a00693af3..b34c69af6 100644 --- a/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java +++ b/src/main/java/io/appium/java_client/android/connection/HasNetworkConnection.java @@ -16,18 +16,14 @@ package io.appium.java_client.android.connection; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.Map; -import static io.appium.java_client.android.AndroidMobileCommandHelper.getNetworkConnectionCommand; -import static io.appium.java_client.android.AndroidMobileCommandHelper.setConnectionCommand; import static java.util.Objects.requireNonNull; -public interface HasNetworkConnection extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasNetworkConnection extends ExecutesMethod { /** * Set the network connection of the device. @@ -37,24 +33,12 @@ public interface HasNetworkConnection extends ExecutesMethod, CanRememberExtensi */ default ConnectionState setConnection(ConnectionState connection) { final String extName = "mobile: setConnectivity"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, Map.of( - "wifi", connection.isWiFiEnabled(), - "data", connection.isDataEnabled(), - "airplaneMode", connection.isAirplaneModeEnabled() - )); - return getConnection(); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return new ConnectionState( - requireNonNull( - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - setConnectionCommand(connection.getBitMask()) - ) - ) - ); - } + CommandExecutionHelper.executeScript(this, extName, Map.of( + "wifi", connection.isWiFiEnabled(), + "data", connection.isDataEnabled(), + "airplaneMode", connection.isAirplaneModeEnabled() + )); + return getConnection(); } /** @@ -64,25 +48,13 @@ default ConnectionState setConnection(ConnectionState connection) { */ default ConnectionState getConnection() { final String extName = "mobile: getConnectivity"; - try { - Map result = requireNonNull( - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName) - ); - return new ConnectionState( - ((boolean) result.get("wifi") ? ConnectionState.WIFI_MASK : 0) - | ((boolean) result.get("data") ? ConnectionState.DATA_MASK : 0) - | ((boolean) result.get("airplaneMode") ? ConnectionState.AIRPLANE_MODE_MASK : 0) - ); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return new ConnectionState( - requireNonNull( - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - getNetworkConnectionCommand() - ) - ) - ); - } + Map result = requireNonNull( + CommandExecutionHelper.executeScript(this, extName) + ); + return new ConnectionState( + ((boolean) result.get("wifi") ? ConnectionState.WIFI_MASK : 0) + | ((boolean) result.get("data") ? ConnectionState.DATA_MASK : 0) + | ((boolean) result.get("airplaneMode") ? ConnectionState.AIRPLANE_MODE_MASK : 0) + ); } } diff --git a/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java b/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java index af633a1d9..fb87a22e9 100644 --- a/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java +++ b/src/main/java/io/appium/java_client/android/nativekey/PressesKey.java @@ -16,18 +16,12 @@ package io.appium.java_client.android.nativekey; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.util.HashMap; -import java.util.Map; -import static io.appium.java_client.MobileCommand.LONG_PRESS_KEY_CODE; -import static io.appium.java_client.MobileCommand.PRESS_KEY_CODE; - -public interface PressesKey extends ExecutesMethod, CanRememberExtensionPresence { +public interface PressesKey extends ExecutesMethod { /** * Send a key event to the device under test. @@ -36,15 +30,7 @@ public interface PressesKey extends ExecutesMethod, CanRememberExtensionPresence */ default void pressKey(KeyEvent keyEvent) { final String extName = "mobile: pressKey"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, keyEvent.build()); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(PRESS_KEY_CODE, keyEvent.build()) - ); - } + CommandExecutionHelper.executeScript(this, extName, keyEvent.build()); } /** @@ -54,16 +40,8 @@ default void pressKey(KeyEvent keyEvent) { */ default void longPressKey(KeyEvent keyEvent) { final String extName = "mobile: pressKey"; - try { - var args = new HashMap<>(keyEvent.build()); - args.put("isLongPress", true); - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute( - markExtensionAbsence(extName), - Map.entry(LONG_PRESS_KEY_CODE, keyEvent.build()) - ); - } + var args = new HashMap<>(keyEvent.build()); + args.put("isLongPress", true); + CommandExecutionHelper.executeScript(this, extName, args); } } diff --git a/src/main/java/io/appium/java_client/clipboard/HasClipboard.java b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java index ae507f940..60cc2e159 100644 --- a/src/main/java/io/appium/java_client/clipboard/HasClipboard.java +++ b/src/main/java/io/appium/java_client/clipboard/HasClipboard.java @@ -16,21 +16,17 @@ package io.appium.java_client.clipboard; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Map; -import static io.appium.java_client.MobileCommand.GET_CLIPBOARD; -import static io.appium.java_client.MobileCommand.SET_CLIPBOARD; import static java.util.Locale.ROOT; import static java.util.Objects.requireNonNull; -public interface HasClipboard extends ExecutesMethod, CanRememberExtensionPresence { +public interface HasClipboard extends ExecutesMethod { /** * Set the content of device's clipboard. * @@ -43,12 +39,7 @@ default void setClipboard(ClipboardContentType contentType, byte[] base64Content "content", new String(requireNonNull(base64Content), StandardCharsets.UTF_8), "contentType", contentType.name().toLowerCase(ROOT) ); - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(this, Map.entry(SET_CLIPBOARD, args)); - } + CommandExecutionHelper.executeScript(this, extName, args); } /** @@ -60,12 +51,7 @@ default void setClipboard(ClipboardContentType contentType, byte[] base64Content default String getClipboard(ClipboardContentType contentType) { final String extName = "mobile: getClipboard"; var args = Map.of("contentType", contentType.name().toLowerCase(ROOT)); - try { - return CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName, args); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - return CommandExecutionHelper.execute(this, Map.entry(GET_CLIPBOARD, args)); - } + return CommandExecutionHelper.executeScript(this, extName, args); } /** diff --git a/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java b/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java deleted file mode 100644 index ebdddaedc..000000000 --- a/src/main/java/io/appium/java_client/ios/IOSMobileCommandHelper.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * See the NOTICE file distributed with this work for additional - * information regarding copyright ownership. - * 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 io.appium.java_client.ios; - -import io.appium.java_client.MobileCommand; - -import java.util.Map; - -@Deprecated -public class IOSMobileCommandHelper extends MobileCommand { - - /** - * This method forms a {@link Map} of parameters for the device shaking. - * - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated this helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> shakeCommand() { - return Map.entry(SHAKE, Map.of()); - } - - /** - * This method forms a {@link Map} of parameters for the touchId simulator. - * - * @param match If true, simulates a successful fingerprint scan. If false, simulates a failed fingerprint scan. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated this helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> touchIdCommand(boolean match) { - return Map.entry(TOUCH_ID, Map.of("match", match)); - } - - /** - * This method forms a {@link Map} of parameters for the toggling touchId - * enrollment in simulator. - * - * @param enabled Whether to enable or disable Touch ID Enrollment for Simulator. - * @return a key-value pair. The key is the command name. The value is a {@link Map} command arguments. - * @deprecated this helper is deprecated and will be removed in future versions. - */ - @Deprecated - public static Map.Entry> toggleTouchIdEnrollmentCommand(boolean enabled) { - return Map.entry(TOUCH_ID_ENROLLMENT, Map.of("enabled", enabled)); - } -} diff --git a/src/main/java/io/appium/java_client/ios/ShakesDevice.java b/src/main/java/io/appium/java_client/ios/ShakesDevice.java index 57302ef8a..7942b231f 100644 --- a/src/main/java/io/appium/java_client/ios/ShakesDevice.java +++ b/src/main/java/io/appium/java_client/ios/ShakesDevice.java @@ -16,25 +16,16 @@ package io.appium.java_client.ios; -import io.appium.java_client.CanRememberExtensionPresence; import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.ExecutesMethod; -import org.openqa.selenium.UnsupportedCommandException; -import static io.appium.java_client.ios.IOSMobileCommandHelper.shakeCommand; - -public interface ShakesDevice extends ExecutesMethod, CanRememberExtensionPresence { +public interface ShakesDevice extends ExecutesMethod { /** * Simulate shaking the Simulator. This API does not work for real devices. */ default void shake() { final String extName = "mobile: shake"; - try { - CommandExecutionHelper.executeScript(assertExtensionExists(extName), extName); - } catch (UnsupportedCommandException e) { - // TODO: Remove the fallback - CommandExecutionHelper.execute(markExtensionAbsence(extName), shakeCommand()); - } + CommandExecutionHelper.executeScript(this, extName); } } diff --git a/src/main/java/io/appium/java_client/windows/WindowsDriver.java b/src/main/java/io/appium/java_client/windows/WindowsDriver.java index 17f8c0fc6..bdbc67161 100644 --- a/src/main/java/io/appium/java_client/windows/WindowsDriver.java +++ b/src/main/java/io/appium/java_client/windows/WindowsDriver.java @@ -18,7 +18,7 @@ import io.appium.java_client.AppiumClientConfig; import io.appium.java_client.AppiumDriver; -import io.appium.java_client.MobileCommand; +import io.appium.java_client.CommandExecutionHelper; import io.appium.java_client.PullsFiles; import io.appium.java_client.PushesFiles; import io.appium.java_client.http.HttpClient; @@ -27,10 +27,19 @@ import io.appium.java_client.screenrecording.CanRecordScreen; import io.appium.java_client.service.local.AppiumDriverLocalService; import io.appium.java_client.service.local.AppiumServiceBuilder; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.Capabilities; import org.openqa.selenium.Platform; import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; + +import static io.appium.java_client.MobileCommand.PULL_FILE; +import static io.appium.java_client.MobileCommand.PULL_FOLDER; +import static io.appium.java_client.MobileCommand.PUSH_FILE; +import static java.util.Objects.requireNonNull; public class WindowsDriver extends AppiumDriver implements PullsFiles, @@ -111,17 +120,57 @@ public WindowsDriver(Capabilities capabilities) { super(ensurePlatformAndAutomationNames(capabilities, PLATFORM_NAME, AUTOMATION_NAME)); } + /** + * Pulls a file from the remote system. The Windows driver has no `mobile:` extension for it. + * + * @param remotePath Path to the file to read data from the remote device. + * @return A byte array of Base64 encoded data. + */ + @Override + public byte[] pullFile(String remotePath) { + return decodeBase64(CommandExecutionHelper.execute(this, Map.entry(PULL_FILE, Map.of("path", remotePath)))); + } + + /** + * Pulls the content of a folder from the remote system. The Windows driver has no `mobile:` extension for it. + * + * @param remotePath Path to the folder to read data from the remote device. + * @return A byte array of Base64 encoded zip archive data. + */ + @Override + public byte[] pullFolder(String remotePath) { + return decodeBase64(CommandExecutionHelper.execute(this, Map.entry(PULL_FOLDER, Map.of("path", remotePath)))); + } + + /** + * Saves base64-encoded data as a file on the remote system. The Windows driver has no `mobile:` extension for it. + * + * @param remotePath Path to the file to write data to on the remote device. + * @param base64Data Base64 encoded byte array of the data to write to the remote device. + */ + @Override + public void pushFile(String remotePath, byte[] base64Data) { + CommandExecutionHelper.execute(this, Map.entry(PUSH_FILE, Map.of( + "path", remotePath, + "data", new String(base64Data, StandardCharsets.UTF_8) + ))); + } + + private static byte[] decodeBase64(@Nullable String base64String) { + return Base64.getDecoder().decode(requireNonNull(base64String).getBytes(StandardCharsets.UTF_8)); + } + /** * Launch the application app under test after it was closed. */ public void launchApp() { - execute(MobileCommand.LAUNCH_APP); + CommandExecutionHelper.executeScript(this, "windows: launchApp"); } /** * Close the app under test. */ public void closeApp() { - execute(MobileCommand.CLOSE_APP); + CommandExecutionHelper.executeScript(this, "windows: closeApp"); } } diff --git a/src/test/java/io/appium/java_client/remote/ExtensionCommandsOverHttpTest.java b/src/test/java/io/appium/java_client/remote/ExtensionCommandsOverHttpTest.java new file mode 100644 index 000000000..c88845982 --- /dev/null +++ b/src/test/java/io/appium/java_client/remote/ExtensionCommandsOverHttpTest.java @@ -0,0 +1,145 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * See the NOTICE file distributed with this work for additional + * information regarding copyright ownership. + * 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 io.appium.java_client.remote; + +import com.google.gson.Gson; +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpServer; +import io.appium.java_client.android.AndroidDriver; +import io.appium.java_client.android.PowerACState; +import io.appium.java_client.android.options.UiAutomator2Options; +import io.appium.java_client.windows.WindowsDriver; +import io.appium.java_client.windows.options.WindowsOptions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Checks the commands that the drivers send for the features that have `mobile:` or `windows:` extensions + * or only the driver-level commands. + */ +class ExtensionCommandsOverHttpTest { + private static final Gson GSON = new Gson(); + + private HttpServer server; + private final List requests = new ArrayList<>(); + private final List> bodies = new ArrayList<>(); + + @BeforeEach + void startServer() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", this::handle); + server.start(); + } + + @AfterEach + void stopServer() { + server.stop(0); + } + + private synchronized void handle(HttpExchange exchange) throws IOException { + var key = exchange.getRequestMethod() + " " + exchange.getRequestURI().getPath(); + var body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8); + requests.add(key); + bodies.add(body.isEmpty() ? Map.of() : GSON.>fromJson(body, Map.class)); + String json; + switch (key) { + case "POST /session": + json = "{\"value\":{\"sessionId\":\"s1\",\"capabilities\":{\"platformName\":\"Android\"}}}"; + break; + case "POST /session/s1/appium/device/pull_file": + case "POST /session/s1/appium/device/pull_folder": + json = "{\"value\":\"" + Base64.getEncoder().encodeToString("data".getBytes(StandardCharsets.UTF_8)) + + "\"}"; + break; + default: + json = "{\"value\":null}"; + break; + } + var bytes = json.getBytes(StandardCharsets.UTF_8); + exchange.getResponseHeaders().add("Content-Type", "application/json; charset=utf-8"); + exchange.sendResponseHeaders(200, bytes.length); + try (OutputStream out = exchange.getResponseBody()) { + out.write(bytes); + } + exchange.close(); + } + + private URL url() throws IOException { + return new URL("http://127.0.0.1:" + server.getAddress().getPort()); + } + + private synchronized Map lastBody() { + return bodies.get(bodies.size() - 1); + } + + private synchronized String lastRequest() { + return requests.get(requests.size() - 1); + } + + @Test + void emulatorPowerStateUsesTheDriverExtension() throws IOException { + var driver = new AndroidDriver(url(), new UiAutomator2Options()); + + driver.setPowerAC(PowerACState.OFF); + + assertEquals("POST /session/s1/execute/sync", lastRequest()); + assertEquals("mobile: powerAc", lastBody().get("script")); + assertEquals(List.of(Map.of("state", "off")), lastBody().get("args")); + } + + @Test + void windowsFilesAreTransferredWithTheDriverCommands() throws IOException { + var driver = new WindowsDriver(url(), new WindowsOptions()); + + assertArrayEquals("data".getBytes(StandardCharsets.UTF_8), driver.pullFile("C:\\a.txt")); + assertEquals("POST /session/s1/appium/device/pull_file", lastRequest()); + assertEquals(Map.of("path", "C:\\a.txt"), lastBody()); + + assertArrayEquals("data".getBytes(StandardCharsets.UTF_8), driver.pullFolder("C:\\dir")); + assertEquals("POST /session/s1/appium/device/pull_folder", lastRequest()); + assertEquals(Map.of("path", "C:\\dir"), lastBody()); + + driver.pushFile("C:\\b.txt", "ZGF0YQ==".getBytes(StandardCharsets.UTF_8)); + assertEquals("POST /session/s1/appium/device/push_file", lastRequest()); + assertEquals(Map.of("path", "C:\\b.txt", "data", "ZGF0YQ=="), lastBody()); + } + + @Test + void windowsAppIsLaunchedAndClosedWithTheDriverExtensions() throws IOException { + var driver = new WindowsDriver(url(), new WindowsOptions()); + + driver.closeApp(); + assertEquals("windows: closeApp", lastBody().get("script")); + + driver.launchApp(); + assertEquals("windows: launchApp", lastBody().get("script")); + } +} diff --git a/src/test/resources/wire-golden/commands.json b/src/test/resources/wire-golden/commands.json index 732228753..debc58f7b 100644 --- a/src/test/resources/wire-golden/commands.json +++ b/src/test/resources/wire-golden/commands.json @@ -15,14 +15,6 @@ }, "body": {} }, - "activateApp": { - "method": "POST", - "uri": "/session/test-session/appium/device/activate_app", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "addCookie": { "method": "POST", "uri": "/session/test-session/cookie", @@ -97,14 +89,6 @@ "headers": {}, "body": null }, - "closeApp": { - "method": "POST", - "uri": "/session/test-session/appium/app/close", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "compareImages": { "method": "POST", "uri": "/session/test-session/appium/compare_images", @@ -113,14 +97,6 @@ }, "body": {} }, - "currentActivity": { - "method": "GET", - "uri": "/session/test-session/appium/device/current_activity", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, "deleteAllCookies": { "method": "DELETE", "uri": "/session/test-session/cookie", @@ -149,14 +125,6 @@ }, "body": null }, - "endTestCoverage": { - "method": "POST", - "uri": "/session/test-session/appium/app/end_test_coverage", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "executeAsyncScript": { "method": "POST", "uri": "/session/test-session/execute/async", @@ -245,14 +213,6 @@ "shadowId": "sample-shadowId" } }, - "fingerPrint": { - "method": "POST", - "uri": "/session/test-session/appium/device/finger_print", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "fullscreenCurrentWindow": { "method": "POST", "uri": "/session/test-session/window/fullscreen", @@ -317,14 +277,6 @@ }, "body": null }, - "getClipboard": { - "method": "POST", - "uri": "/session/test-session/appium/device/get_clipboard", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "getContextHandles": { "method": "GET", "uri": "/session/test-session/contexts", @@ -365,14 +317,6 @@ }, "body": null }, - "getCurrentPackage": { - "method": "GET", - "uri": "/session/test-session/appium/device/current_package", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, "getCurrentUrl": { "method": "GET", "uri": "/session/test-session/url", @@ -397,22 +341,6 @@ }, "body": null }, - "getDeviceTime": { - "method": "GET", - "uri": "/session/test-session/appium/device/system_time", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, - "getDisplayDensity": { - "method": "GET", - "uri": "/session/test-session/appium/device/display_density", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, "getElementAccessibleName": { "method": "GET", "uri": "/session/test-session/element/sample-id/computedlabel", @@ -536,14 +464,6 @@ }, "body": {} }, - "getNetworkConnection": { - "method": "GET", - "uri": "/session/test-session/network_connection", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, "getPageSource": { "method": "GET", "uri": "/session/test-session/source", @@ -552,14 +472,6 @@ }, "body": null }, - "getPerformanceData": { - "method": "POST", - "uri": "/session/test-session/appium/getPerformanceData", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "getScreenOrientation": { "method": "GET", "uri": "/session/test-session/orientation", @@ -592,30 +504,6 @@ }, "body": null }, - "getStrings": { - "method": "POST", - "uri": "/session/test-session/appium/app/strings", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "getSuppportedPerformanceDataTypes": { - "method": "POST", - "uri": "/session/test-session/appium/performanceData/types", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "getSystemBars": { - "method": "GET", - "uri": "/session/test-session/appium/device/system_bars", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, "getTimeouts": { "method": "GET", "uri": "/session/test-session/timeouts", @@ -664,38 +552,6 @@ }, "body": {} }, - "gsmCall": { - "method": "POST", - "uri": "/session/test-session/appium/device/gsm_call", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "gsmSignal": { - "method": "POST", - "uri": "/session/test-session/appium/device/gsm_signal", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "gsmVoice": { - "method": "POST", - "uri": "/session/test-session/appium/device/gsm_voice", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "hideKeyboard": { - "method": "POST", - "uri": "/session/test-session/appium/device/hide_keyboard", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "implicitlyWait": { "method": "POST", "uri": "/session/test-session/timeouts/implicit_wait", @@ -704,22 +560,6 @@ }, "body": {} }, - "installApp": { - "method": "POST", - "uri": "/session/test-session/appium/device/install_app", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "isAppInstalled": { - "method": "POST", - "uri": "/session/test-session/appium/device/app_installed", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "isElementDisplayed": { "method": "GET", "uri": "/session/test-session/element/sample-id/displayed", @@ -744,38 +584,6 @@ }, "body": null }, - "isKeyboardShown": { - "method": "GET", - "uri": "/session/test-session/appium/device/is_keyboard_shown", - "headers": { - "cache-control": "no-cache" - }, - "body": null - }, - "isLocked": { - "method": "POST", - "uri": "/session/test-session/appium/device/is_locked", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "launchApp": { - "method": "POST", - "uri": "/session/test-session/appium/app/launch", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "lock": { - "method": "POST", - "uri": "/session/test-session/appium/device/lock", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "logCustomEvent": { "method": "POST", "uri": "/session/test-session/appium/log_event", @@ -784,14 +592,6 @@ }, "body": {} }, - "longPressKeyCode": { - "method": "POST", - "uri": "/session/test-session/appium/device/long_press_keycode", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "maximizeCurrentWindow": { "method": "POST", "uri": "/session/test-session/window/maximize", @@ -808,14 +608,6 @@ }, "body": {} }, - "networkSpeed": { - "method": "POST", - "uri": "/session/test-session/appium/device/network_speed", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "newSession": { "method": "POST", "uri": "/session", @@ -832,38 +624,6 @@ }, "body": {} }, - "openNotifications": { - "method": "POST", - "uri": "/session/test-session/appium/device/open_notifications", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "powerAC": { - "method": "POST", - "uri": "/session/test-session/appium/device/power_ac", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "powerCapacity": { - "method": "POST", - "uri": "/session/test-session/appium/device/power_capacity", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "pressKeyCode": { - "method": "POST", - "uri": "/session/test-session/appium/device/press_keycode", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "printPage": { "method": "POST", "uri": "/session/test-session/print", @@ -896,14 +656,6 @@ }, "body": {} }, - "queryAppState": { - "method": "POST", - "uri": "/session/test-session/appium/device/app_state", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "quit": { "method": "DELETE", "uri": "/session/test-session", @@ -924,14 +676,6 @@ "headers": {}, "body": null }, - "removeApp": { - "method": "POST", - "uri": "/session/test-session/appium/device/remove_app", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "removeCredential": { "method": "DELETE", "uri": "/session/test-session/webauthn/authenticator/sample-authenticatorId/credentials/sample-credentialId", @@ -944,24 +688,6 @@ "headers": {}, "body": null }, - "replaceValue": { - "method": "POST", - "uri": "/session/test-session/appium/element/sample-id/replace_value", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": { - "id": "sample-id" - } - }, - "reset": { - "method": "POST", - "uri": "/session/test-session/appium/app/reset", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "resetCooldown": { "method": "POST", "uri": "/session/test-session/fedcm/resetcooldown", @@ -970,14 +696,6 @@ }, "body": {} }, - "runAppInBackground": { - "method": "POST", - "uri": "/session/test-session/appium/app/background", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "screenshot": { "method": "GET", "uri": "/session/test-session/screenshot", @@ -995,14 +713,6 @@ "body": {} }, "sendKeysToElement": "error:NullPointerException", - "sendSMS": { - "method": "POST", - "uri": "/session/test-session/appium/device/send_sms", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "setAlertValue": { "method": "POST", "uri": "/session/test-session/alert/text", @@ -1011,14 +721,6 @@ }, "body": {} }, - "setClipboard": { - "method": "POST", - "uri": "/session/test-session/appium/device/set_clipboard", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "setCurrentWindowSize": { "method": "POST", "uri": "/session/test-session/window/rect", @@ -1043,14 +745,6 @@ }, "body": {} }, - "setNetworkConnection": { - "method": "POST", - "uri": "/session/test-session/network_connection", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "setScreenOrientation": { "method": "POST", "uri": "/session/test-session/orientation", @@ -1101,16 +795,6 @@ "authenticatorId": "sample-authenticatorId" } }, - "setValue": { - "method": "POST", - "uri": "/session/test-session/appium/element/sample-id/value", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": { - "id": "sample-id" - } - }, "setWindowPosition": { "method": "POST", "uri": "/session/test-session/window/rect", @@ -1119,22 +803,6 @@ }, "body": {} }, - "shake": { - "method": "POST", - "uri": "/session/test-session/appium/device/shake", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "startActivity": { - "method": "POST", - "uri": "/session/test-session/appium/device/start_activity", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, "startRecordingScreen": { "method": "POST", "uri": "/session/test-session/appium/start_recording_screen", @@ -1191,69 +859,5 @@ "content-type": "application/json; charset=utf-8" }, "body": {} - }, - "terminateApp": { - "method": "POST", - "uri": "/session/test-session/appium/device/terminate_app", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "toggleData": { - "method": "POST", - "uri": "/session/test-session/appium/device/toggle_data", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "toggleEnrollTouchId": { - "method": "POST", - "uri": "/session/test-session/appium/simulator/toggle_touch_id_enrollment", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "toggleFlightMode": { - "method": "POST", - "uri": "/session/test-session/appium/device/toggle_airplane_mode", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "toggleLocationServices": { - "method": "POST", - "uri": "/session/test-session/appium/device/toggle_location_services", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "toggleWiFi": { - "method": "POST", - "uri": "/session/test-session/appium/device/toggle_wifi", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "touchId": { - "method": "POST", - "uri": "/session/test-session/appium/simulator/touch_id", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} - }, - "unlock": { - "method": "POST", - "uri": "/session/test-session/appium/device/unlock", - "headers": { - "content-type": "application/json; charset=utf-8" - }, - "body": {} } } From a7a28695b1a601f48e4578554ba807bd4434ff87 Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Mon, 5 Oct 2026 20:49:17 +0200 Subject: [PATCH 5/6] docs: describe the Appium server versions that the client supports Co-Authored-By: Claude Sonnet 5.5 --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 43de7c290..eefac8ccb 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,13 @@ build tools. Sometimes users may want to pin used Selenium dependencies for [var Follow the [Transitive Dependencies Management article](docs/transitive-dependencies-management.md) for more information about establishing a fixed Selenium version for your Java test framework. +### Appium Server Compatibility + +A major release of the client guarantees support only for the Appium server API of the major server version that is +current at the time of the release and of the previous major version. Endpoints and commands that were removed or +deprecated in the server earlier may not be supported anymore. For example, if Appium 4 is the current version, the API +that existed in Appium 2 and was removed or deprecated in Appium 3 is not guaranteed to work. + ## Drivers Support Appium java client has dedicated classes to support the following Appium drivers: From b7ab4e868fe1675f02b62b22fe353a86b401589d Mon Sep 17 00:00:00 2001 From: Mykola Mokhnach Date: Tue, 6 Oct 2026 18:23:47 +0200 Subject: [PATCH 6/6] test: make the wait steps of AndroidFunctionTest null-safe Function#compose and Function#andThen pass null on, unlike the removed AppiumFunction, which skipped the next step. FluentWait treats null as "keep polling", so the steps have to handle a null input themselves. Co-Authored-By: Claude Sonnet 5.5 --- .../io/appium/java_client/android/AndroidFunctionTest.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java index 514996ab9..9174b22d5 100644 --- a/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java +++ b/src/e2eAndroidTest/java/io/appium/java_client/android/AndroidFunctionTest.java @@ -27,7 +27,11 @@ public class AndroidFunctionTest extends BaseAndroidTest { + // Function#compose and Function#andThen pass null on, so the steps of a wait must handle it themselves private final Function> searchingFunction = input -> { + if (input == null) { + return null; + } List result = input.findElements(By.tagName("a")); if (result.size() > 0) { @@ -52,6 +56,9 @@ public class AndroidFunctionTest extends BaseAndroidTest { }; private final Function, List> filteringFunction = input -> { + if (input == null) { + return null; + } final List result = new ArrayList<>(); input.forEach(element -> { if (element.getText().equals("Hello World! - 1")) {