From a729d738cbdf3d9a674ebab9d5236181e9e4b4c6 Mon Sep 17 00:00:00 2001 From: Sergey Tikhomirov Date: Fri, 12 Sep 2014 20:02:20 +0400 Subject: [PATCH 1/3] New interfaces. The beginning of #105 fix --- .../io/appium/java_client/AppiumDriver.java | 5 + .../java_client/DeviceActionShortcuts.java | 22 +++++ .../io/appium/java_client/IOSKeyCode.java | 2 +- .../java_client/InteractsWithFiles.java | 30 ++++++ .../io/appium/java_client/MobileDriver.java | 12 +-- .../java_client/PerformsTouchActions.java | 7 ++ .../java/io/appium/java_client/ScrollsTo.java | 9 ++ .../io/appium/java_client/TouchShortcuts.java | 99 +++++++++++++++++++ .../android/AndroidDeviceActionShortcuts.java | 18 ++++ .../{ => android}/AndroidKeyMetastate.java | 2 +- .../java_client/android/HasAppStrings.java | 21 ++++ .../android/HasNetworkConnection.java | 24 +++++ .../java_client/android/PushesFiles.java | 17 ++++ .../java_client/android/StartsActivity.java | 30 ++++++ .../java_client/ios/GetsNamedTextField.java | 17 ++++ .../ios/IOSDeviceActionShortcuts.java | 37 +++++++ .../java_client/MobileDriverAndroidTest.java | 1 + 17 files changed, 342 insertions(+), 11 deletions(-) create mode 100644 src/main/java/io/appium/java_client/DeviceActionShortcuts.java create mode 100644 src/main/java/io/appium/java_client/InteractsWithFiles.java create mode 100644 src/main/java/io/appium/java_client/PerformsTouchActions.java create mode 100644 src/main/java/io/appium/java_client/ScrollsTo.java create mode 100644 src/main/java/io/appium/java_client/TouchShortcuts.java create mode 100644 src/main/java/io/appium/java_client/android/AndroidDeviceActionShortcuts.java rename src/main/java/io/appium/java_client/{ => android}/AndroidKeyMetastate.java (96%) create mode 100644 src/main/java/io/appium/java_client/android/HasAppStrings.java create mode 100644 src/main/java/io/appium/java_client/android/HasNetworkConnection.java create mode 100644 src/main/java/io/appium/java_client/android/PushesFiles.java create mode 100644 src/main/java/io/appium/java_client/android/StartsActivity.java create mode 100644 src/main/java/io/appium/java_client/ios/GetsNamedTextField.java create mode 100644 src/main/java/io/appium/java_client/ios/IOSDeviceActionShortcuts.java diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 206e5b328..492acf79b 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -170,6 +170,7 @@ public void sendKeyEvent(int key, Integer metastate) { /** * Get the current activity being run on the mobile device */ + //Should be moved to the subclass (Android) public String currentActivity() { Response response = execute(CURRENT_ACTIVITY); return response.getValue().toString(); @@ -304,6 +305,7 @@ public void runAppInBackground(int seconds) { * Open the notification shade, on Android devices. * Android only method. */ + //Should be moved to the subclass public void openNotifications() { execute(OPEN_NOTIFICATIONS); } /** * Performs a chain of touch actions, which together can be considered an entire gesture. @@ -541,6 +543,7 @@ public void closeApp() { * @param intent intent to broadcast * @param path path to .ec file */ + //Should be moved to the subclass public void endTestCoverage(String intent, String path) { ImmutableMap.Builder builder = ImmutableMap.builder(); builder.put("intent", intent).put("path", path); @@ -561,6 +564,7 @@ public void lockScreen(int seconds) { * * @return true if device is locked. False otherwise */ + //Should be moved to the subclass public boolean isLocked() { Response response = execute(IS_LOCKED); @@ -659,6 +663,7 @@ private void setSetting(AppiumSetting setting, Object value) { * * @param compress ignores unimportant views if true, doesn't ignore otherwise. */ + //Should be moved to the subclass public void ignoreUnimportantViews(Boolean compress) { setSetting(AppiumSetting.IGNORE_UNIMPORTANT_VIEWS, compress); } diff --git a/src/main/java/io/appium/java_client/DeviceActionShortcuts.java b/src/main/java/io/appium/java_client/DeviceActionShortcuts.java new file mode 100644 index 000000000..ee9bdf6d8 --- /dev/null +++ b/src/main/java/io/appium/java_client/DeviceActionShortcuts.java @@ -0,0 +1,22 @@ +package io.appium.java_client; + +public interface DeviceActionShortcuts { + + /** + * Hides the keyboard if it is showing. + * On iOS, there are multiple strategies for hiding the keyboard. Defaults to the "tapOutside" strategy (taps outside the keyboard). + * Switch to using hideKeyboard(HideKeyboardStrategy.PRESS_KEY, "Done") if this doesn't work. + */ + public void hideKeyboard(); + + /** + * Send a key event to the device + * + * @param key code for the key pressed on the device + * + * @see AndroidKeyCode + * @see IOSKeyCode + */ + public void sendKeyEvent(int key); + +} diff --git a/src/main/java/io/appium/java_client/IOSKeyCode.java b/src/main/java/io/appium/java_client/IOSKeyCode.java index f76921bb4..6d8c5219d 100644 --- a/src/main/java/io/appium/java_client/IOSKeyCode.java +++ b/src/main/java/io/appium/java_client/IOSKeyCode.java @@ -18,7 +18,7 @@ package io.appium.java_client; /** - * Some common key codes for Android Key Events + * Some common key codes for iOS Key Events */ public interface IOSKeyCode { diff --git a/src/main/java/io/appium/java_client/InteractsWithFiles.java b/src/main/java/io/appium/java_client/InteractsWithFiles.java new file mode 100644 index 000000000..d84f0a9ab --- /dev/null +++ b/src/main/java/io/appium/java_client/InteractsWithFiles.java @@ -0,0 +1,30 @@ +package io.appium.java_client; + +public interface InteractsWithFiles { + + /** + * + * @param remotePath + * On Android and iOS, this is either the path to the file + * (relative to the root of the app's file system). On iOS only, + * if path starts with /AppName.app, which will be replaced with + * the application's .app directory + * @return A byte array of Base64 encoded data. + */ + public byte[] pullFile(String remotePath); + + /** + * Pull a folder from the simulator/device. Does not work on iOS Real + * Devices, but works on simulators + * + * @param remotePath + * On Android and iOS, this is either the path to the file + * (relative to the root of the app's file system). On iOS only, + * if path starts with /AppName.app, which will be replaced with + * the application's .app directory + * @return A byte array of Base64 encoded data, representing a ZIP ARCHIVE + * of the contents of the requested folder. + */ + public byte[] pullFolder(String remotePath); + +} diff --git a/src/main/java/io/appium/java_client/MobileDriver.java b/src/main/java/io/appium/java_client/MobileDriver.java index f876b59ac..9523175a6 100644 --- a/src/main/java/io/appium/java_client/MobileDriver.java +++ b/src/main/java/io/appium/java_client/MobileDriver.java @@ -17,19 +17,13 @@ package io.appium.java_client; - import org.openqa.selenium.ContextAware; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.Response; import java.util.Map; -public interface MobileDriver extends WebDriver, ContextAware { - - - public Response execute(String driverCommand, Map parameters); - - public TouchAction performTouchAction(TouchAction touchAction); - - public void performMultiTouchAction(MultiTouchAction multiAction); +public interface MobileDriver extends WebDriver, ContextAware, + PerformsTouchActions { + public Response execute(String driverCommand, Map parameters); } diff --git a/src/main/java/io/appium/java_client/PerformsTouchActions.java b/src/main/java/io/appium/java_client/PerformsTouchActions.java new file mode 100644 index 000000000..c0d6e7791 --- /dev/null +++ b/src/main/java/io/appium/java_client/PerformsTouchActions.java @@ -0,0 +1,7 @@ +package io.appium.java_client; + +public interface PerformsTouchActions { + public TouchAction performTouchAction(TouchAction touchAction); + + public void performMultiTouchAction(MultiTouchAction multiAction); +} diff --git a/src/main/java/io/appium/java_client/ScrollsTo.java b/src/main/java/io/appium/java_client/ScrollsTo.java new file mode 100644 index 000000000..bfb127b5e --- /dev/null +++ b/src/main/java/io/appium/java_client/ScrollsTo.java @@ -0,0 +1,9 @@ +package io.appium.java_client; + +public interface ScrollsTo { + + public MobileElement scrollTo(String text); + + public MobileElement scrollToExact(String text); + +} diff --git a/src/main/java/io/appium/java_client/TouchShortcuts.java b/src/main/java/io/appium/java_client/TouchShortcuts.java new file mode 100644 index 000000000..22e914da4 --- /dev/null +++ b/src/main/java/io/appium/java_client/TouchShortcuts.java @@ -0,0 +1,99 @@ +package io.appium.java_client; + +import org.openqa.selenium.WebElement; + +public interface TouchShortcuts { + + /** + * Convenience method for "zooming in" on an element on the screen. + * "zooming in" refers to the action of two appendages pressing the screen and sliding away from each other. + * NOTE: + * This convenience method slides touches away from the element, if this would happen to place one of them + * off the screen, appium will return an outOfBounds error. In this case, revert to using the MultiTouchAction api + * instead of this method. + * + * @param x x coordinate to start zoom on + * @param y y coordinate to start zoom on + */ + public void zoom(int x, int y); + + /** + * Convenience method for "zooming in" on an element on the screen. + * "zooming in" refers to the action of two appendages pressing the screen and sliding away from each other. + * NOTE: + * This convenience method slides touches away from the element, if this would happen to place one of them + * off the screen, appium will return an outOfBounds error. In this case, revert to using the MultiTouchAction api + * instead of this method. + * + * @param el The element to pinch + */ + public void zoom(WebElement el); + + /** + * Convenience method for tapping a position on the screen + * + * @param fingers + * number of fingers/appendages to tap with + * @param x + * x coordinate + * @param y + * y coordinate + * @param duration + */ + public void tap(int fingers, int x, int y, int duration); + + /** + * Convenience method for tapping the center of an element on the screen + * + * @param fingers + * number of fingers/appendages to tap with + * @param element + * element to tap + * @param duration + * how long between pressing down, and lifting fingers/appendages + */ + public void tap(int fingers, WebElement element, int duration); + + /** + * Convenience method for swiping across the screen + * + * @param startx + * starting x coordinate + * @param starty + * starting y coordinate + * @param endx + * ending x coordinate + * @param endy + * ending y coordinate + * @param duration + * amount of time in milliseconds for the entire swipe action to + * take + */ + public void swipe(int startx, int starty, int endx, int endy, int duration); + + /** + * Convenience method for pinching an element on the screen. + * "pinching" refers to the action of two appendages pressing the screen and sliding towards each other. + * NOTE: + * This convenience method places the initial touches around the element at a distance, if this would happen to place + * one of them off the screen, appium will return an outOfBounds error. In this case, revert to using the + * MultiTouchAction api instead of this method. + * + * @param x x coordinate to terminate the pinch on + * @param y y coordinate to terminate the pinch on + */ + public void pinch(int x, int y); + + /** + * Convenience method for pinching an element on the screen. + * "pinching" refers to the action of two appendages pressing the screen and sliding towards each other. + * NOTE: + * This convenience method places the initial touches around the element, if this would happen to place one of them + * off the screen, appium with return an outOfBounds error. In this case, revert to using the MultiTouchAction api + * instead of this method. + * + * @param el The element to pinch + */ + public void pinch(WebElement el); + +} diff --git a/src/main/java/io/appium/java_client/android/AndroidDeviceActionShortcuts.java b/src/main/java/io/appium/java_client/android/AndroidDeviceActionShortcuts.java new file mode 100644 index 000000000..2bd971aa9 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidDeviceActionShortcuts.java @@ -0,0 +1,18 @@ +package io.appium.java_client.android; + +import io.appium.java_client.AndroidKeyCode; +import io.appium.java_client.DeviceActionShortcuts; + +public interface AndroidDeviceActionShortcuts extends DeviceActionShortcuts { + /** + * Send a key event along with an Android metastate to an Android device + * Metastates are things like *shift* to get uppercase characters + * + * @param key code for the key pressed on the Android device + * @param metastate metastate for the keypress + * + * @see AndroidKeyCode + * @see AndroidKeyMetastate + */ + public void sendKeyEvent(int key, Integer metastate); +} diff --git a/src/main/java/io/appium/java_client/AndroidKeyMetastate.java b/src/main/java/io/appium/java_client/android/AndroidKeyMetastate.java similarity index 96% rename from src/main/java/io/appium/java_client/AndroidKeyMetastate.java rename to src/main/java/io/appium/java_client/android/AndroidKeyMetastate.java index 0bf56ad75..cd2d50277 100644 --- a/src/main/java/io/appium/java_client/AndroidKeyMetastate.java +++ b/src/main/java/io/appium/java_client/android/AndroidKeyMetastate.java @@ -15,7 +15,7 @@ +limitations under the License. + */ -package io.appium.java_client; +package io.appium.java_client.android; /** * Metastates for Android Key Events diff --git a/src/main/java/io/appium/java_client/android/HasAppStrings.java b/src/main/java/io/appium/java_client/android/HasAppStrings.java new file mode 100644 index 000000000..a4f619322 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/HasAppStrings.java @@ -0,0 +1,21 @@ +package io.appium.java_client.android; + + +public interface HasAppStrings { + + /** + * Get all defined Strings from an Android app for the default language + * + * @return a string of all the localized strings defined in the app + */ + public String getAppStrings(); + + /** + * Get all defined Strings from an Android app for the specified language + * + * @param language strings language code + * @return a string of all the localized strings defined in the app + */ + public String getAppStrings(String language); + +} diff --git a/src/main/java/io/appium/java_client/android/HasNetworkConnection.java b/src/main/java/io/appium/java_client/android/HasNetworkConnection.java new file mode 100644 index 000000000..51d4c5c99 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/HasNetworkConnection.java @@ -0,0 +1,24 @@ +package io.appium.java_client.android; + +import io.appium.java_client.NetworkConnectionSetting; + +public interface HasNetworkConnection { + + /** + * Get the current network settings of the device. + * + * @return NetworkConnectionSetting objects will let you inspect the status + * of AirplaneMode, Wifi, Data connections + */ + public NetworkConnectionSetting getNetworkConnection(); + + /** + * Set the network connection of the device. This is an Android-only method + * + * @param connection + * The NetworkConnectionSetting configuration to use for the + * device + */ + public void setNetworkConnection(NetworkConnectionSetting connection); + +} diff --git a/src/main/java/io/appium/java_client/android/PushesFiles.java b/src/main/java/io/appium/java_client/android/PushesFiles.java new file mode 100644 index 000000000..8c1d81c86 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/PushesFiles.java @@ -0,0 +1,17 @@ +package io.appium.java_client.android; + +import io.appium.java_client.InteractsWithFiles; + +public interface PushesFiles extends InteractsWithFiles { + + /** + * Save base64 encoded data as a file on the remote mobile device. + * + * @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 + */ + public void pushFile(String remotePath, byte[] base64Data); + +} diff --git a/src/main/java/io/appium/java_client/android/StartsActivity.java b/src/main/java/io/appium/java_client/android/StartsActivity.java new file mode 100644 index 000000000..3dd9eaf59 --- /dev/null +++ b/src/main/java/io/appium/java_client/android/StartsActivity.java @@ -0,0 +1,30 @@ +package io.appium.java_client.android; + +public interface StartsActivity { + /** + * This method should start arbitrary activity during a test. If the activity belongs to + * another application, that application is started and the activity is opened. + * + * @param appPackage The package containing the activity. [Required] + * @param appActivity The activity to start. [Required] + * @param appWaitPackage Automation will begin after this package starts. [Optional] + * @param appWaitActivity Automation will begin after this activity starts. [Optional] + * @example + * *.startActivity("com.foo.bar", ".MyActivity", null, null); + */ + public void startActivity(String appPackage, String appActivity, String appWaitPackage, String appWaitActivity) + throws IllegalArgumentException; + + /** + * This method should start arbitrary activity during a test. If the activity belongs to + * another application, that application is started and the activity is opened. + * + * @param appPackage The package containing the activity. [Required] + * @param appActivity The activity to start. [Required] + * @example + * *.startActivity("com.foo.bar", ".MyActivity"); + */ + public void startActivity(String appPackage, String appActivity) + throws IllegalArgumentException; + +} diff --git a/src/main/java/io/appium/java_client/ios/GetsNamedTextField.java b/src/main/java/io/appium/java_client/ios/GetsNamedTextField.java new file mode 100644 index 000000000..6898c769c --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/GetsNamedTextField.java @@ -0,0 +1,17 @@ +package io.appium.java_client.ios; + +import org.openqa.selenium.WebElement; + +public interface GetsNamedTextField { + /** + * In iOS apps, named TextFields have the same accessibility Id as their + * containing TableElement. This is a convenience method for getting the + * named TextField, rather than its containing element. + * + * @param name + * accessiblity id of TextField + * @return The textfield with the given accessibility id + */ + public WebElement getNamedTextField(String name); + +} diff --git a/src/main/java/io/appium/java_client/ios/IOSDeviceActionShortcuts.java b/src/main/java/io/appium/java_client/ios/IOSDeviceActionShortcuts.java new file mode 100644 index 000000000..e8f0e2c5f --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSDeviceActionShortcuts.java @@ -0,0 +1,37 @@ +package io.appium.java_client.ios; + +import io.appium.java_client.DeviceActionShortcuts; + +public interface IOSDeviceActionShortcuts extends DeviceActionShortcuts { + + /** + * Hides the keyboard by pressing the button specified by keyName if it is + * showing. + * + * @param keyName + * The button pressed by the mobile driver to attempt hiding the + * keyboard + */ + public void hideKeyboard(String keyName); + + /** + * Hides the keyboard if it is showing. Available strategies are PRESS_KEY + * and TAP_OUTSIDE. One taps outside the keyboard, the other presses a key + * of your choosing (probably the 'Done' key). Hiding the keyboard often + * depends on the way an app is implemented, no single strategy always + * works. + * + * @param strategy + * HideKeyboardStrategy + * @param keyName + * a String, representing the text displayed on the button of the + * keyboard you want to press. For example: "Done" + */ + public void hideKeyboard(String strategy, String keyName); + + /** + * Simulate shaking the device + */ + public void shake(); + +} diff --git a/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java b/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java index 2a4cbfd71..db2c285b8 100644 --- a/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java +++ b/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java @@ -17,6 +17,7 @@ package io.appium.java_client; +import io.appium.java_client.android.AndroidKeyMetastate; import io.appium.java_client.remote.MobileCapabilityType; import io.appium.java_client.remote.MobilePlatform; From 59c14865c2f5bc7e3f99c145ebfb545356486487 Mon Sep 17 00:00:00 2001 From: Sergey Tikhomirov Date: Tue, 16 Sep 2014 21:15:29 +0400 Subject: [PATCH 2/3] #105 #108 AndroidDriver. AppiumDriver. Implement & improvement. --- .../io/appium/java_client/AppiumDriver.java | 1469 ++++++++--------- .../java_client/PerformsTouchActions.java | 24 + .../java_client/android/AndroidDriver.java | 240 +++ .../java_client/MobileDriverAndroidTest.java | 8 +- 4 files changed, 980 insertions(+), 761 deletions(-) create mode 100644 src/main/java/io/appium/java_client/android/AndroidDriver.java diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 492acf79b..16479369b 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -17,769 +17,724 @@ package io.appium.java_client; -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; +import static io.appium.java_client.MobileCommand.CLOSE_APP; +import static io.appium.java_client.MobileCommand.COMPLEX_FIND; +import static io.appium.java_client.MobileCommand.CURRENT_ACTIVITY; +import static io.appium.java_client.MobileCommand.END_TEST_COVERAGE; +import static io.appium.java_client.MobileCommand.GET_NETWORK_CONNECTION; +import static io.appium.java_client.MobileCommand.GET_SETTINGS; +import static io.appium.java_client.MobileCommand.GET_STRINGS; +import static io.appium.java_client.MobileCommand.HIDE_KEYBOARD; +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.IS_LOCKED; +import static io.appium.java_client.MobileCommand.KEY_EVENT; +import static io.appium.java_client.MobileCommand.LAUNCH_APP; +import static io.appium.java_client.MobileCommand.LOCK; +import static io.appium.java_client.MobileCommand.OPEN_NOTIFICATIONS; +import static io.appium.java_client.MobileCommand.PERFORM_MULTI_TOUCH; +import static io.appium.java_client.MobileCommand.PERFORM_TOUCH_ACTION; +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 io.appium.java_client.MobileCommand.REMOVE_APP; +import static io.appium.java_client.MobileCommand.RESET; +import static io.appium.java_client.MobileCommand.RUN_APP_IN_BACKGROUND; +import static io.appium.java_client.MobileCommand.SET_NETWORK_CONNECTION; +import static io.appium.java_client.MobileCommand.SET_SETTINGS; +import static io.appium.java_client.MobileCommand.SET_VALUE; +import static io.appium.java_client.MobileCommand.SHAKE; +import static io.appium.java_client.MobileCommand.START_ACTIVITY; import io.appium.java_client.internal.JsonToMobileElementConverter; -import org.openqa.selenium.*; -import org.openqa.selenium.html5.Location; -import org.openqa.selenium.html5.LocationContext; -import org.openqa.selenium.remote.*; -import org.openqa.selenium.remote.html5.RemoteLocationContext; -import org.openqa.selenium.remote.http.HttpMethod; +import io.appium.java_client.remote.MobileCapabilityType; -import javax.xml.bind.DatatypeConverter; import java.net.URL; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import static com.google.common.base.Preconditions.checkArgument; -import static io.appium.java_client.remote.MobileCapabilityType.*; -import static io.appium.java_client.MobileCommand.*; - -public class AppiumDriver extends RemoteWebDriver implements MobileDriver, ContextAware, Rotatable, FindsByIosUIAutomation, - FindsByAndroidUIAutomator, FindsByAccessibilityId, LocationContext { - - private final static ErrorHandler errorHandler = new ErrorHandler(new ErrorCodesMobile(), true); - private URL remoteAddress; - private RemoteLocationContext locationContext; - private ExecuteMethod executeMethod; - - public AppiumDriver(URL remoteAddress, Capabilities desiredCapabilities){ - - super(remoteAddress, desiredCapabilities); - this.setElementConverter(new JsonToMobileElementConverter(this)); - - this.executeMethod = new AppiumExecutionMethod(this); - this.remoteAddress = remoteAddress; - locationContext = new RemoteLocationContext(executeMethod); - - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder - .put(RESET, postC("/session/:sessionId/appium/app/reset")) - .put(GET_STRINGS, postC("/session/:sessionId/appium/app/strings")) - .put(KEY_EVENT, postC("/session/:sessionId/appium/device/keyevent")) - .put(CURRENT_ACTIVITY, getC("/session/:sessionId/appium/device/current_activity")) - .put(SET_VALUE, postC("/session/:sessionId/appium/element/:id/value")) - .put(PULL_FILE, postC("/session/:sessionId/appium/device/pull_file")) - .put(PULL_FOLDER, postC("/session/:sessionId/appium/device/pull_folder")) - .put(HIDE_KEYBOARD, postC("/session/:sessionId/appium/device/hide_keyboard")) - .put(PUSH_FILE, postC("/session/:sessionId/appium/device/push_file")) - .put(RUN_APP_IN_BACKGROUND, postC("/session/:sessionId/appium/app/background")) - .put(PERFORM_TOUCH_ACTION, postC("/session/:sessionId/touch/perform")) - .put(PERFORM_MULTI_TOUCH, postC("/session/:sessionId/touch/multi/perform")) - .put(IS_APP_INSTALLED, postC("/session/:sessionId/appium/device/app_installed")) - .put(INSTALL_APP, postC("/session/:sessionId/appium/device/install_app")) - .put(REMOVE_APP, postC("/session/:sessionId/appium/device/remove_app")) - .put(LAUNCH_APP, postC("/session/:sessionId/appium/app/launch")) - .put(CLOSE_APP, postC("/session/:sessionId/appium/app/close")) - .put(END_TEST_COVERAGE, postC("/session/:sessionId/appium/app/end_test_coverage")) - .put(LOCK, postC("/session/:sessionId/appium/device/lock")) - .put(IS_LOCKED, postC("/session/:sessionId/appium/device/is_locked")) - .put(SHAKE, postC("/session/:sessionId/appium/device/shake")) - .put(COMPLEX_FIND, postC("/session/:sessionId/appium/app/complex_find")) - .put(OPEN_NOTIFICATIONS, postC("/session/:sessionId/appium/device/open_notifications")) - .put(GET_NETWORK_CONNECTION, getC("/session/:sessionId/network_connection")) - .put(SET_NETWORK_CONNECTION, postC("/session/:sessionId/network_connection")) - .put(GET_SETTINGS, getC("/session/:sessionId/appium/settings")) - .put(SET_SETTINGS, postC("/session/:sessionId/appium/settings")) - .put(START_ACTIVITY, postC("/session/:sessionId/appium/device/start_activity")) - ; - ImmutableMap mobileCommands = builder.build(); - - HttpCommandExecutor mobileExecutor = new HttpCommandExecutor(mobileCommands, remoteAddress); - super.setCommandExecutor(mobileExecutor); - - super.setErrorHandler(errorHandler); - } - - @Override - public Response execute(String driverCommand, Map parameters) { - - return super.execute(driverCommand, parameters); - } - - @Override - protected Response execute(String command) { - return execute(command, ImmutableMap.of()); - } - - @Override - public ExecuteMethod getExecuteMethod() { - return executeMethod; - } - - /** - * Reset the currently running app for this session - */ - public void resetApp() { - execute(MobileCommand.RESET); - } - - /** - * Get all defined Strings from an Android app for the default language - * - * @return a string of all the localized strings defined in the app - */ - public String getAppStrings() { - Response response = execute(GET_STRINGS); - return response.getValue().toString(); - } - - /** - * Get all defined Strings from an Android app for the specified language - * - * @param language strings language code - * @return a string of all the localized strings defined in the app - */ - public String getAppStrings(String language) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("language", language); - ImmutableMap parameters = builder.build(); - Response response = execute(GET_STRINGS, parameters); - return response.getValue().toString(); - } - - /** - * Send a key event to the device - * - * @param key code for the key pressed on the device - */ - public void sendKeyEvent(int key) { - sendKeyEvent(key, null); - } - - /** - * Send a key event along with an Android metastate to an Android device - * Metastates are things like *shift* to get uppercase characters - * - * @param key code for the key pressed on the Android device - * @param metastate metastate for the keypress - */ - public void sendKeyEvent(int key, Integer metastate) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("keycode", key); - if (metastate != null) { builder.put("metastate", metastate); } - ImmutableMap parameters = builder.build(); - execute(KEY_EVENT, parameters); - } - - /** - * Get the current activity being run on the mobile device - */ - //Should be moved to the subclass (Android) - public String currentActivity() { - Response response = execute(CURRENT_ACTIVITY); - return response.getValue().toString(); - } - - /** - * Launches an arbitrary activity during a test. If the activity belongs to - * another application, that application is started and the activity is opened. - * - * This is an Android-only method. - * @param appPackage The package containing the activity. [Required] - * @param appActivity The activity to start. [Required] - * @param appWaitPackage Automation will begin after this package starts. [Optional] - * @param appWaitActivity Automation will begin after this activity starts. [Optional] - * @example - * driver.startActivity("com.foo.bar", ".MyActivity", null, null); - */ - public void startActivity(String appPackage, String appActivity, String appWaitPackage, String appWaitActivity) - throws IllegalArgumentException { - - checkArgument((_isNotNullOrEmpty(appPackage) && _isNotNullOrEmpty(appActivity)), - String.format("'%s' and '%s' are required.", APP_PACKAGE, APP_ACTIVITY)); - - appWaitPackage = _isNotNullOrEmpty(appWaitPackage) ? appWaitPackage : ""; - appWaitActivity = _isNotNullOrEmpty(appWaitActivity) ? appWaitActivity : ""; - - ImmutableMap parameters = ImmutableMap.of(APP_PACKAGE, appPackage, - APP_ACTIVITY, appActivity, - APP_WAIT_PACKAGE, appWaitPackage, - APP_WAIT_ACTIVITY, appWaitActivity); - - execute(START_ACTIVITY, parameters); - } - - /** - * Checks if a string is null, empty, or whitespace. - * - * @param str String to check. - * - * @return True if str is not null or empty. - */ - private static boolean _isNotNullOrEmpty(String str) { - return str != null && !str.isEmpty() && str.trim().length() > 0; - } - - /** - * - * @param remotePath On Android and iOS, this is either the path to the file (relative to the root of the app's file system). - * On iOS only, if path starts with /AppName.app, which will be replaced with the application's .app directory - * @return A byte array of Base64 encoded data. - */ - public byte[] pullFile(String remotePath) { - Response response = execute(PULL_FILE, ImmutableMap.of("path", remotePath)); - String base64String = response.getValue().toString(); - - return DatatypeConverter.parseBase64Binary(base64String); - } - - /** - * Save base64 encoded data as a file on the remote mobile device. - * This is an Android only method. - * @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 - */ - public void pushFile(String remotePath, byte[] base64Data) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("path", remotePath).put("data", base64Data); - execute(PUSH_FILE, builder.build()); - } - - /** - * Pull a folder from the simulator/device. Does not work on iOS Real Devices, but works on simulators - * - * @param remotePath On Android and iOS, this is either the path to the file (relative to the root of the app's file system). - * On iOS only, if path starts with /AppName.app, which will be replaced with the application's .app directory - * @return A byte array of Base64 encoded data, representing a ZIP ARCHIVE of the contents of the requested folder. - */ - public byte[] pullFolder(String remotePath) { - Response response = execute(PULL_FOLDER, ImmutableMap.of("path", remotePath)); - String base64String = response.getValue().toString(); - - return DatatypeConverter.parseBase64Binary(base64String); - } - - /** - * Hides the keyboard if it is showing. - * On iOS, there are multiple strategies for hiding the keyboard. Defaults to the "tapOutside" strategy (taps outside the keyboard). - * Switch to using hideKeyboard(HideKeyboardStrategy.PRESS_KEY, "Done") if this doesn't work. - */ - public void hideKeyboard() { - execute(HIDE_KEYBOARD); - } - - /** - * Hides the keyboard if it is showing. Available strategies are PRESS_KEY and TAP_OUTSIDE. - * One taps outside the keyboard, the other presses a key of your choosing (probably the 'Done' key). - * Hiding the keyboard often depends on the way an app is implemented, no single strategy always works. - * - * These parameters are only for iOS, and ignored by Android. - * - * @param strategy HideKeyboardStrategy - * @param keyName a String, representing the text displayed on the button of the keyboard you want to press. For example: "Done" - */ - public void hideKeyboard(String strategy, String keyName) { - ImmutableMap parameters = ImmutableMap.of("strategy", strategy); - if (_isNotNullOrEmpty(keyName)) { - parameters = parameters.of("key", keyName); - } - - execute(HIDE_KEYBOARD, parameters); - } - - /** - * Hides the keyboard by pressing the button specified by keyName if it is showing. - * This is an iOS only command. - * @param keyName The button pressed by the mobile driver to attempt hiding the keyboard - */ - public void hideKeyboard(String keyName) { - execute(HIDE_KEYBOARD, ImmutableMap.of("keyName", keyName)); - } - - /** - * Runs the current app as a background app for the number of seconds requested. - * This is a synchronous method, it returns after the back has been returned to the foreground. - * @param seconds Number of seconds to run App in background - */ - public void runAppInBackground(int seconds) { - execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", seconds)); - } - - /** - * Open the notification shade, on Android devices. - * Android only method. - */ - //Should be moved to the subclass - public void openNotifications() { execute(OPEN_NOTIFICATIONS); } - /** - * Performs a chain of touch actions, which together can be considered an entire gesture. - * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html - * - * It's more convenient to call the perform() method of the TouchAction object itself. - * - * @param touchAction A TouchAction object, which contains a list of individual touch actions to perform - * @return the same touchaction object - */ - public TouchAction performTouchAction(TouchAction touchAction) { - ImmutableMap parameters = touchAction.getParameters(); - touchAction.clearParameters(); - execute(PERFORM_TOUCH_ACTION, parameters); - - return touchAction; - } - - /** - * Performs multiple TouchAction gestures at the same time, to simulate multiple fingers/touch inputs. - * See the Webriver 3 spec https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html - * - * It's more convenient to call the perform() method of the MultiTouchAction object. - * - * @param multiAction the MultiTouchAction object to perform. - */ - public void performMultiTouchAction(MultiTouchAction multiAction) { - ImmutableMap parameters = multiAction.getParameters(); - execute(PERFORM_MULTI_TOUCH, parameters); - } - - /** - * Convenience method for tapping the center of an element on the screen - * @param fingers number of fingers/appendages to tap with - * @param element element to tap - * @param duration how long between pressing down, and lifting fingers/appendages - */ - public void tap(int fingers, WebElement element, int duration) { - MultiTouchAction multiTouch = new MultiTouchAction(this); - - for (int i = 0; i < fingers; i++) { - multiTouch.add(createTap(element, duration)); - } - - multiTouch.perform(); - } - - /** - * Convenience method for tapping a position on the screen - * @param fingers number of fingers/appendages to tap with - * @param x x coordinate - * @param y y coordinate - * @param duration - */ - public void tap(int fingers, int x, int y, int duration) { - MultiTouchAction multiTouch = new MultiTouchAction(this); - - for (int i = 0; i < fingers; i++) { - multiTouch.add(createTap(x, y, duration)); - } - - multiTouch.perform(); - } - - /** - * Convenience method for swiping across the screen - * @param startx starting x coordinate - * @param starty starting y coordinate - * @param endx ending x coordinate - * @param endy ending y coordinate - * @param duration amount of time in milliseconds for the entire swipe action to take - */ - public void swipe(int startx, int starty, int endx, int endy, int duration) { - TouchAction touchAction = new TouchAction(this); - - //appium converts press-wait-moveto-release to a swipe action - touchAction.press(startx, starty).waitAction(duration).moveTo(endx, endy).release(); - - touchAction.perform(); - } - - /** - * Convenience method for pinching an element on the screen. - * "pinching" refers to the action of two appendages pressing the screen and sliding towards each other. - * NOTE: - * This convenience method places the initial touches around the element, if this would happen to place one of them - * off the screen, appium with return an outOfBounds error. In this case, revert to using the MultiTouchAction api - * instead of this method. - * - * @param el The element to pinch - */ - public void pinch(WebElement el) { - MultiTouchAction multiTouch = new MultiTouchAction(this); - - Dimension dimensions = el.getSize(); - Point upperLeft = el.getLocation(); - Point center = new Point(upperLeft.getX() + dimensions.getWidth() / 2, upperLeft.getY() + dimensions.getHeight() / 2); - - TouchAction action0 = new TouchAction(this).press(el, center.getX(), center.getY() - 100).moveTo(el).release(); - TouchAction action1 = new TouchAction(this).press(el, center.getX(), center.getY() + 100).moveTo(el).release(); - - multiTouch.add(action0).add(action1); - - multiTouch.perform(); - } - - /** - * Convenience method for pinching an element on the screen. - * "pinching" refers to the action of two appendages pressing the screen and sliding towards each other. - * NOTE: - * This convenience method places the initial touches around the element at a distance, if this would happen to place - * one of them off the screen, appium will return an outOfBounds error. In this case, revert to using the - * MultiTouchAction api instead of this method. - * - * @param x x coordinate to terminate the pinch on - * @param y y coordinate to terminate the pinch on - */ - public void pinch(int x, int y) { - MultiTouchAction multiTouch = new MultiTouchAction(this); - - TouchAction action0 = new TouchAction(this).press(x, y-100).moveTo(x, y).release(); - TouchAction action1 = new TouchAction(this).press(x, y+100).moveTo(x, y).release(); - - multiTouch.add(action0).add(action1); - - multiTouch.perform(); - } - - /** - * Convenience method for "zooming in" on an element on the screen. - * "zooming in" refers to the action of two appendages pressing the screen and sliding away from each other. - * NOTE: - * This convenience method slides touches away from the element, if this would happen to place one of them - * off the screen, appium will return an outOfBounds error. In this case, revert to using the MultiTouchAction api - * instead of this method. - * - * @param el The element to pinch - */ - public void zoom(WebElement el) { - MultiTouchAction multiTouch = new MultiTouchAction(this); - - Dimension dimensions = el.getSize(); - Point upperLeft = el.getLocation(); - Point center = new Point(upperLeft.getX() + dimensions.getWidth() / 2, upperLeft.getY() + dimensions.getHeight() / 2); - - TouchAction action0 = new TouchAction(this).press(el).moveTo(el, center.getX(), center.getY() - 100).release(); - TouchAction action1 = new TouchAction(this).press(el).moveTo(el, center.getX(), center.getY() + 100).release(); - - multiTouch.add(action0).add(action1); - - multiTouch.perform(); - } - - /** - * Convenience method for "zooming in" on an element on the screen. - * "zooming in" refers to the action of two appendages pressing the screen and sliding away from each other. - * NOTE: - * This convenience method slides touches away from the element, if this would happen to place one of them - * off the screen, appium will return an outOfBounds error. In this case, revert to using the MultiTouchAction api - * instead of this method. - * - * @param x x coordinate to start zoom on - * @param y y coordinate to start zoom on - */ - public void zoom(int x, int y) { - MultiTouchAction multiTouch = new MultiTouchAction(this); - - TouchAction action0 = new TouchAction(this).press(x, y).moveTo(x, y-100).release(); - TouchAction action1 = new TouchAction(this).press(x, y).moveTo(x, y+100).release(); - - multiTouch.add(action0).add(action1); - - multiTouch.perform(); - } - - /** - * In iOS apps, named TextFields have the same accessibility Id as their containing TableElement. - * This is a convenience method for getting the named TextField, rather than its containing element. - * @param name accessiblity id of TextField - * @return The textfield with the given accessibility id - */ - public WebElement getNamedTextField(String name) { - MobileElement element = (MobileElement) findElementByAccessibilityId(name); - if (element.getTagName() != "TextField") { - return element.findElementByAccessibilityId(name); - } - - return element; - } - - /** - * Checks if an app is installed on the device - * @param bundleId bundleId of the app - * @return True if app is installed, false otherwise - */ - public boolean isAppInstalled(String bundleId) { - Response response = execute(IS_APP_INSTALLED, ImmutableMap.of("bundleId", bundleId)); - - return Boolean.parseBoolean(response.getValue().toString()); - } - - /** - * Install an app on the mobile device - * @param appPath path to app to install - */ - public void installApp(String appPath) { - execute(INSTALL_APP, ImmutableMap.of("appPath", appPath)); - } - - /** - * Remove the specified app from the device (uninstall) - * @param bundleId the bunble identifier (or app id) of the app to remove - */ - public void removeApp(String bundleId) { - execute(REMOVE_APP, ImmutableMap.of("bundleId", bundleId)); - } - - /** - * Launch the app which was provided in the capabilities at session creation - */ - public void launchApp() { - execute(LAUNCH_APP); - } - - /** - * Close the app which was provided in the capabilities at session creation - */ - public void closeApp() { - execute(CLOSE_APP); - } - - /** - * Get test-coverage data - * Android-only method - * @param intent intent to broadcast - * @param path path to .ec file - */ - //Should be moved to the subclass - public void endTestCoverage(String intent, String path) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("intent", intent).put("path", path); - execute(END_TEST_COVERAGE, builder.build()); - } - - /** - * Lock the device (bring it to the lock screen) for a given number of seconds - * @param seconds number of seconds to lock the screen for - */ - public void lockScreen(int seconds) { - execute(LOCK, ImmutableMap.of("seconds", seconds)); - } - - /** - * Check if the device is locked. - * *Android Only Method* - * - * @return true if device is locked. False otherwise - */ - //Should be moved to the subclass - public boolean isLocked() { - - Response response = execute(IS_LOCKED); - - return Boolean.parseBoolean(response.getValue().toString()); - } - - /** - * Simulate shaking the device - * This is an iOS-only method - */ - public void shake() { - execute(SHAKE); - } - - /** - * Get the current network settings of the device. - * This is an Android-only method - * - * @return NetworkConnectionSetting objects will let you inspect the status of AirplaneMode, Wifi, Data connections - */ - public NetworkConnectionSetting getNetworkConnection() { - Response response = execute(GET_NETWORK_CONNECTION); - - return new NetworkConnectionSetting(Integer.parseInt(response.getValue().toString())); - } - - /** - * Set the network connection of the device. - * This is an Android-only method - * - * @param connection The NetworkConnectionSetting configuration to use for the device - */ - public void setNetworkConnection(NetworkConnectionSetting connection) { - // the new version of the webdriver protocol is going forward with sending JSON message which look like - // {name: "name of endpoint", parameters: "JSON parameters"} - // this is for webdrivers which run on protocols besides HTTP (like TCP) - // we're implementing that pattern here, for this new method, but haven't translated it to all other commands yet - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("name", "network_connection") - .put("parameters", ImmutableMap.of("type", connection.value)); - - execute(SET_NETWORK_CONNECTION, builder.build()); - } - - /** - * Get settings stored for this test session - * It's probably better to use a convenience function, rather than use this function directly. - * Try finding the method for the specific setting you want to read - * - * @return JsonObject, a straight-up hash of settings - */ - public JsonObject getSettings() { - Response response = execute(GET_SETTINGS); - - JsonParser parser = new JsonParser(); - JsonObject settings = (JsonObject)parser.parse(response.getValue().toString()); - - return settings; - } - - /** - * Set settings for this test session - * It's probably better to use a convenience function, rather than use this function directly. - * Try finding the method for the specific setting you want to change - * - * @param settings Map of setting keys and values - */ - private void setSettings(ImmutableMap settings) { - - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put("settings", settings); - - execute(SET_SETTINGS, builder.build()); - } - - /** - * Set a setting for this test session - * It's probably better to use a convenience function, rather than use this function directly. - * Try finding the method for the specific setting you want to change - * - * @param setting AppiumSetting you wish to set - * @param value value of the setting - */ - private void setSetting(AppiumSetting setting, Object value) { - ImmutableMap.Builder builder = ImmutableMap.builder(); - builder.put(setting.toString(), value); - setSettings(builder.build()); - } - - /** - * Set the `ignoreUnimportantViews` setting. - * *Android-only method* - * - * Sets whether Android devices should use `setCompressedLayoutHeirarchy()` which ignores all views which are marked IMPORTANT_FOR_ACCESSIBILITY_NO or IMPORTANT_FOR_ACCESSIBILITY_AUTO (and have been deemed not important by the system), in an attempt to make things less confusing or faster. - * - * @param compress ignores unimportant views if true, doesn't ignore otherwise. - */ - //Should be moved to the subclass - public void ignoreUnimportantViews(Boolean compress) { - setSetting(AppiumSetting.IGNORE_UNIMPORTANT_VIEWS, compress); - } - - @Override - public WebDriver context(String name) { - if (!_isNotNullOrEmpty(name)) { - throw new IllegalArgumentException("Must supply a context name"); - } - - execute(DriverCommand.SWITCH_TO_CONTEXT, ImmutableMap.of("name", name)); - return AppiumDriver.this; - } - - @Override - public Set getContextHandles() { - Response response = execute(DriverCommand.GET_CONTEXT_HANDLES); - Object value = response.getValue(); - try { - List returnedValues = (List)value; - return new LinkedHashSet(returnedValues); - } catch (ClassCastException ex) { - throw new WebDriverException("Returned value cannot be converted to List: " + value, ex); - } - } - - @Override - public String getContext() { - String contextName = String.valueOf(execute(DriverCommand.GET_CURRENT_CONTEXT_HANDLE).getValue()); - if (contextName.equals("null")) { - return null; - } - return contextName; - } - - @Override - public void rotate(ScreenOrientation orientation) { - execute(DriverCommand.SET_SCREEN_ORIENTATION, ImmutableMap.of("orientation", orientation.value().toUpperCase())); - } - - @Override - public ScreenOrientation getOrientation() { - Response response = execute(DriverCommand.GET_SCREEN_ORIENTATION); - String orientation = response.getValue().toString().toLowerCase(); - if (orientation.equals(ScreenOrientation.LANDSCAPE.value())) { - return ScreenOrientation.LANDSCAPE; - } else if (orientation.equals(ScreenOrientation.PORTRAIT.value())) { - return ScreenOrientation.PORTRAIT; - } else { - throw new WebDriverException("Unexpected orientation returned: " + orientation); - } - } - - @Override - public WebElement findElementByIosUIAutomation(String using) { - return findElement("-ios uiautomation", using); - } - - @Override - public List findElementsByIosUIAutomation(String using) { - return findElements("-ios uiautomation", using); - } - - @Override - public WebElement findElementByAndroidUIAutomator(String using) { - return findElement("-android uiautomator", using); - } - - @Override - public List findElementsByAndroidUIAutomator(String using) { - return findElements("-android uiautomator", using); - } - - @Override - public WebElement findElementByAccessibilityId(String using) { - return findElement("accessibility id", using); - } - - @Override - public List findElementsByAccessibilityId(String using) { - return findElements("accessibility id", using); - } - - @Override - public Location location() { - return locationContext.location(); - } - - @Override - public void setLocation(Location location) { - locationContext.setLocation(location); - } - - private TouchAction createTap(WebElement element, int duration) { - TouchAction tap = new TouchAction(this); - return tap.press(element).waitAction(duration).release(); - } - - private TouchAction createTap(int x, int y, int duration) { - TouchAction tap = new TouchAction(this); - return tap.press(x, y).waitAction(duration).release(); - } - - private static CommandInfo getC(String url) { - return new CommandInfo(url, HttpMethod.GET); - } - - private static CommandInfo postC(String url) { - return new CommandInfo(url, HttpMethod.POST); - } - - private static CommandInfo deleteC(String url) { - return new CommandInfo(url, HttpMethod.DELETE); - } - - public URL getRemoteAddress() { - return remoteAddress; - } +import javax.xml.bind.DatatypeConverter; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.ContextAware; +import org.openqa.selenium.Dimension; +import org.openqa.selenium.Point; +import org.openqa.selenium.Rotatable; +import org.openqa.selenium.ScreenOrientation; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebDriverException; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.html5.Location; +import org.openqa.selenium.html5.LocationContext; +import org.openqa.selenium.remote.CommandInfo; +import org.openqa.selenium.remote.DesiredCapabilities; +import org.openqa.selenium.remote.DriverCommand; +import org.openqa.selenium.remote.ErrorHandler; +import org.openqa.selenium.remote.ExecuteMethod; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.Response; +import org.openqa.selenium.remote.html5.RemoteLocationContext; +import org.openqa.selenium.remote.http.HttpMethod; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +public class AppiumDriver extends RemoteWebDriver implements MobileDriver, + ContextAware, Rotatable, FindsByIosUIAutomation, + FindsByAndroidUIAutomator, FindsByAccessibilityId, LocationContext, + DeviceActionShortcuts, TouchShortcuts, InteractsWithFiles { + + private final static ErrorHandler errorHandler = new ErrorHandler( + new ErrorCodesMobile(), true); + private URL remoteAddress; + private RemoteLocationContext locationContext; + private ExecuteMethod executeMethod; + + //frequently used command parameters + protected final String KEY_CODE = "keycode"; + protected final String PATH = "path"; + private final String SETTINGS = "settings"; + + /** + * @param originalCapabilities + * the given {@link Capabilities} + * @param newPlatform + * a {@link MobileCapabilityType#PLATFORM_NAME} value which has + * to be set up + * @return {@link Capabilities} with changed mobile platform value + */ + protected static Capabilities substituteMobilePlatform( + Capabilities originalCapabilities, String newPlatform) { + DesiredCapabilities dc = new DesiredCapabilities(originalCapabilities); + dc.setCapability(MobileCapabilityType.PLATFORM_NAME, newPlatform); + return dc; + } + + /** + * @param param + * is a parameter name + * @param value + * is the parameter value + * @return built {@link ImmutableMap} + */ + protected static ImmutableMap getCommandImmutableMap( + String param, Object value) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(param, value); + return builder.build(); + } + + /** + * + * @param params is the array with parameter names + * @param values is the array with parameter values + * @return built {@link ImmutableMap} + */ + protected static ImmutableMap getCommandImmutableMap( + String[] params, Object[] values) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (int i=0; i < params.length; i++ ){ + if (_isNotNullOrEmpty(params[i])){ + builder.put(params[i], values[i]); + } + } + return builder.build(); + } + + public AppiumDriver(URL remoteAddress, Capabilities desiredCapabilities) { + + super(remoteAddress, desiredCapabilities); + this.setElementConverter(new JsonToMobileElementConverter(this)); + + this.executeMethod = new AppiumExecutionMethod(this); + this.remoteAddress = remoteAddress; + locationContext = new RemoteLocationContext(executeMethod); + + ImmutableMap.Builder builder = ImmutableMap + .builder(); + builder.put(RESET, postC("/session/:sessionId/appium/app/reset")) + .put(GET_STRINGS, + postC("/session/:sessionId/appium/app/strings")) + .put(KEY_EVENT, + postC("/session/:sessionId/appium/device/keyevent")) + .put(CURRENT_ACTIVITY, + getC("/session/:sessionId/appium/device/current_activity")) + .put(SET_VALUE, + postC("/session/:sessionId/appium/element/:id/value")) + .put(PULL_FILE, + postC("/session/:sessionId/appium/device/pull_file")) + .put(PULL_FOLDER, + postC("/session/:sessionId/appium/device/pull_folder")) + .put(HIDE_KEYBOARD, + postC("/session/:sessionId/appium/device/hide_keyboard")) + .put(PUSH_FILE, + postC("/session/:sessionId/appium/device/push_file")) + .put(RUN_APP_IN_BACKGROUND, + postC("/session/:sessionId/appium/app/background")) + .put(PERFORM_TOUCH_ACTION, + postC("/session/:sessionId/touch/perform")) + .put(PERFORM_MULTI_TOUCH, + postC("/session/:sessionId/touch/multi/perform")) + .put(IS_APP_INSTALLED, + postC("/session/:sessionId/appium/device/app_installed")) + .put(INSTALL_APP, + postC("/session/:sessionId/appium/device/install_app")) + .put(REMOVE_APP, + postC("/session/:sessionId/appium/device/remove_app")) + .put(LAUNCH_APP, postC("/session/:sessionId/appium/app/launch")) + .put(CLOSE_APP, postC("/session/:sessionId/appium/app/close")) + .put(END_TEST_COVERAGE, + postC("/session/:sessionId/appium/app/end_test_coverage")) + .put(LOCK, postC("/session/:sessionId/appium/device/lock")) + .put(IS_LOCKED, + postC("/session/:sessionId/appium/device/is_locked")) + .put(SHAKE, postC("/session/:sessionId/appium/device/shake")) + .put(COMPLEX_FIND, + postC("/session/:sessionId/appium/app/complex_find")) + .put(OPEN_NOTIFICATIONS, + postC("/session/:sessionId/appium/device/open_notifications")) + .put(GET_NETWORK_CONNECTION, + getC("/session/:sessionId/network_connection")) + .put(SET_NETWORK_CONNECTION, + postC("/session/:sessionId/network_connection")) + .put(GET_SETTINGS, getC("/session/:sessionId/appium/settings")) + .put(SET_SETTINGS, postC("/session/:sessionId/appium/settings")) + .put(START_ACTIVITY, + postC("/session/:sessionId/appium/device/start_activity")); + ImmutableMap mobileCommands = builder.build(); + + HttpCommandExecutor mobileExecutor = new HttpCommandExecutor( + mobileCommands, remoteAddress); + super.setCommandExecutor(mobileExecutor); + + super.setErrorHandler(errorHandler); + } + + @Override + public Response execute(String driverCommand, Map parameters) { + + return super.execute(driverCommand, parameters); + } + + @Override + protected Response execute(String command) { + return execute(command, ImmutableMap. of()); + } + + @Override + public ExecuteMethod getExecuteMethod() { + return executeMethod; + } + + /** + * Reset the currently running app for this session + */ + public void resetApp() { + execute(MobileCommand.RESET); + } + + /** + * Send a key event to the device + * + * @param key + * code for the key pressed on the device + */ + @Override + public void sendKeyEvent(int key) { + execute(KEY_EVENT, getCommandImmutableMap(KEY_CODE, key)); + } + + /** + * Checks if a string is null, empty, or whitespace. + * + * @param str + * String to check. + * + * @return True if str is not null or empty. + */ + protected static boolean _isNotNullOrEmpty(String str) { + return str != null && !str.isEmpty() && str.trim().length() > 0; + } + + /** + * @see InteractsWithFiles#pullFile(String) + */ + @Override + public byte[] pullFile(String remotePath) { + Response response = execute(PULL_FILE, + ImmutableMap.of(PATH, remotePath)); + String base64String = response.getValue().toString(); + + return DatatypeConverter.parseBase64Binary(base64String); + } + + /** + * @see InteractsWithFiles#pullFolder(String) + */ + @Override + public byte[] pullFolder(String remotePath) { + Response response = execute(PULL_FOLDER, + ImmutableMap.of(PATH, remotePath)); + String base64String = response.getValue().toString(); + + return DatatypeConverter.parseBase64Binary(base64String); + } + + /** + * @see DeviceActionShortcuts#hideKeyboard() + */ + @Override + public void hideKeyboard() { + execute(HIDE_KEYBOARD); + } + + /** + * Hides the keyboard if it is showing. Available strategies are PRESS_KEY + * and TAP_OUTSIDE. One taps outside the keyboard, the other presses a key + * of your choosing (probably the 'Done' key). Hiding the keyboard often + * depends on the way an app is implemented, no single strategy always + * works. + * + * These parameters are only for iOS, and ignored by Android. + * + * @param strategy + * HideKeyboardStrategy + * @param keyName + * a String, representing the text displayed on the button of the + * keyboard you want to press. For example: "Done" + */ + //Should be moved to the subclass + public void hideKeyboard(String strategy, String keyName) { + String[] parameters = new String[] { "strategy", "key" }; + Object[] values = new Object[] { strategy, keyName }; + execute(HIDE_KEYBOARD, getCommandImmutableMap(parameters, values)); + } + + /** + * Hides the keyboard by pressing the button specified by keyName if it is + * showing. This is an iOS only command. + * + * @param keyName + * The button pressed by the mobile driver to attempt hiding the + * keyboard + */ + //Should be moved to the subclass + public void hideKeyboard(String keyName) { + execute(HIDE_KEYBOARD, ImmutableMap.of("keyName", keyName)); + } + + /** + * Runs the current app as a background app for the number of seconds + * requested. This is a synchronous method, it returns after the back has + * been returned to the foreground. + * + * @param seconds + * Number of seconds to run App in background + */ + public void runAppInBackground(int seconds) { + execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", seconds)); + } + + /** + * @see PerformsTouchActions#performTouchAction(TouchAction) + */ + @SuppressWarnings("rawtypes") + @Override + public TouchAction performTouchAction(TouchAction touchAction) { + ImmutableMap parameters = touchAction + .getParameters(); + touchAction.clearParameters(); + execute(PERFORM_TOUCH_ACTION, parameters); + + return touchAction; + } + + /** + * @see PerformsTouchActions#performMultiTouchAction(MultiTouchAction) + */ + @Override + @SuppressWarnings({ "rawtypes", "unchecked" }) + public void performMultiTouchAction(MultiTouchAction multiAction) { + ImmutableMap parameters = multiAction + .getParameters(); + execute(PERFORM_MULTI_TOUCH, parameters); + } + + /** + *@see TouchShortcuts#tap(int, WebElement, int) + */ + @Override + public void tap(int fingers, WebElement element, int duration) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + for (int i = 0; i < fingers; i++) { + multiTouch.add(createTap(element, duration)); + } + + multiTouch.perform(); + } + + /** + * @see TouchShortcuts#tap(int, int, int, int) + */ + @Override + public void tap(int fingers, int x, int y, int duration) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + for (int i = 0; i < fingers; i++) { + multiTouch.add(createTap(x, y, duration)); + } + + multiTouch.perform(); + } + + /** + * @see TouchShortcuts#swipe(int, int, int, int, int) + */ + @Override + public void swipe(int startx, int starty, int endx, int endy, int duration) { + TouchAction touchAction = new TouchAction(this); + + // appium converts press-wait-moveto-release to a swipe action + touchAction.press(startx, starty).waitAction(duration) + .moveTo(endx, endy).release(); + + touchAction.perform(); + } + + /** + * @see TouchShortcuts#pinch(WebElement) + */ + @Override + public void pinch(WebElement el) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + Dimension dimensions = el.getSize(); + Point upperLeft = el.getLocation(); + Point center = new Point(upperLeft.getX() + dimensions.getWidth() / 2, + upperLeft.getY() + dimensions.getHeight() / 2); + + TouchAction action0 = new TouchAction(this) + .press(el, center.getX(), center.getY() - 100).moveTo(el) + .release(); + TouchAction action1 = new TouchAction(this) + .press(el, center.getX(), center.getY() + 100).moveTo(el) + .release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * @see TouchShortcuts#pinch(int, int) + */ + @Override + public void pinch(int x, int y) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + TouchAction action0 = new TouchAction(this).press(x, y - 100) + .moveTo(x, y).release(); + TouchAction action1 = new TouchAction(this).press(x, y + 100) + .moveTo(x, y).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * @see TouchShortcuts#zoom(WebElement) + */ + @Override + public void zoom(WebElement el) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + Dimension dimensions = el.getSize(); + Point upperLeft = el.getLocation(); + Point center = new Point(upperLeft.getX() + dimensions.getWidth() / 2, + upperLeft.getY() + dimensions.getHeight() / 2); + + TouchAction action0 = new TouchAction(this).press(el) + .moveTo(el, center.getX(), center.getY() - 100).release(); + TouchAction action1 = new TouchAction(this).press(el) + .moveTo(el, center.getX(), center.getY() + 100).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * @see TouchShortcuts#zoom(int, int) + */ + @Override + public void zoom(int x, int y) { + MultiTouchAction multiTouch = new MultiTouchAction(this); + + TouchAction action0 = new TouchAction(this).press(x, y) + .moveTo(x, y - 100).release(); + TouchAction action1 = new TouchAction(this).press(x, y) + .moveTo(x, y + 100).release(); + + multiTouch.add(action0).add(action1); + + multiTouch.perform(); + } + + /** + * In iOS apps, named TextFields have the same accessibility Id as their + * containing TableElement. This is a convenience method for getting the + * named TextField, rather than its containing element. + * + * @param name + * accessiblity id of TextField + * @return The textfield with the given accessibility id + */ + //Should be moved to the subclass + public WebElement getNamedTextField(String name) { + MobileElement element = (MobileElement) findElementByAccessibilityId(name); + if (element.getTagName() != "TextField") { + return element.findElementByAccessibilityId(name); + } + + return element; + } + + /** + * Checks if an app is installed on the device + * + * @param bundleId + * bundleId of the app + * @return True if app is installed, false otherwise + */ + public boolean isAppInstalled(String bundleId) { + Response response = execute(IS_APP_INSTALLED, + ImmutableMap.of("bundleId", bundleId)); + + return Boolean.parseBoolean(response.getValue().toString()); + } + + /** + * Install an app on the mobile device + * + * @param appPath + * path to app to install + */ + public void installApp(String appPath) { + execute(INSTALL_APP, ImmutableMap.of("appPath", appPath)); + } + + /** + * Remove the specified app from the device (uninstall) + * + * @param bundleId + * the bunble identifier (or app id) of the app to remove + */ + public void removeApp(String bundleId) { + execute(REMOVE_APP, ImmutableMap.of("bundleId", bundleId)); + } + + /** + * Launch the app which was provided in the capabilities at session creation + */ + public void launchApp() { + execute(LAUNCH_APP); + } + + /** + * Close the app which was provided in the capabilities at session creation + */ + public void closeApp() { + execute(CLOSE_APP); + } + + /** + * Lock the device (bring it to the lock screen) for a given number of + * seconds + * + * @param seconds + * number of seconds to lock the screen for + */ + //Should be moved to the subclass (supposed to be in iOS) + public void lockScreen(int seconds) { + execute(LOCK, ImmutableMap.of("seconds", seconds)); + } + + /** + * Simulate shaking the device This is an iOS-only method + */ + //Should be moved to the subclass + public void shake() { + execute(SHAKE); + } + + /** + * Get settings stored for this test session It's probably better to use a + * convenience function, rather than use this function directly. Try finding + * the method for the specific setting you want to read + * + * @return JsonObject, a straight-up hash of settings + */ + public JsonObject getSettings() { + Response response = execute(GET_SETTINGS); + + JsonParser parser = new JsonParser(); + JsonObject settings = (JsonObject) parser.parse(response.getValue() + .toString()); + + return settings; + } + + /** + * Set settings for this test session It's probably better to use a + * convenience function, rather than use this function directly. Try finding + * the method for the specific setting you want to change + * + * @param settings + * Map of setting keys and values + */ + private void setSettings(ImmutableMap settings) { + execute(SET_SETTINGS, getCommandImmutableMap(SETTINGS, settings)); + } + + /** + * Set a setting for this test session It's probably better to use a + * convenience function, rather than use this function directly. Try finding + * the method for the specific setting you want to change + * + * @param setting + * AppiumSetting you wish to set + * @param value + * value of the setting + */ + protected void setSetting(AppiumSetting setting, Object value) { + setSettings(getCommandImmutableMap(setting.toString(), value)); + } + + @Override + public WebDriver context(String name) { + if (!_isNotNullOrEmpty(name)) { + throw new IllegalArgumentException("Must supply a context name"); + } + + execute(DriverCommand.SWITCH_TO_CONTEXT, ImmutableMap.of("name", name)); + return AppiumDriver.this; + } + + @SuppressWarnings("unchecked") + @Override + public Set getContextHandles() { + Response response = execute(DriverCommand.GET_CONTEXT_HANDLES); + Object value = response.getValue(); + try { + List returnedValues = (List) value; + return new LinkedHashSet(returnedValues); + } catch (ClassCastException ex) { + throw new WebDriverException( + "Returned value cannot be converted to List: " + + value, ex); + } + } + + @Override + public String getContext() { + String contextName = String.valueOf(execute( + DriverCommand.GET_CURRENT_CONTEXT_HANDLE).getValue()); + if (contextName.equals("null")) { + return null; + } + return contextName; + } + + @Override + public void rotate(ScreenOrientation orientation) { + execute(DriverCommand.SET_SCREEN_ORIENTATION, ImmutableMap.of( + "orientation", orientation.value().toUpperCase())); + } + + @Override + public ScreenOrientation getOrientation() { + Response response = execute(DriverCommand.GET_SCREEN_ORIENTATION); + String orientation = response.getValue().toString().toLowerCase(); + if (orientation.equals(ScreenOrientation.LANDSCAPE.value())) { + return ScreenOrientation.LANDSCAPE; + } else if (orientation.equals(ScreenOrientation.PORTRAIT.value())) { + return ScreenOrientation.PORTRAIT; + } else { + throw new WebDriverException("Unexpected orientation returned: " + + orientation); + } + } + + @Override + public WebElement findElementByIosUIAutomation(String using) { + return findElement("-ios uiautomation", using); + } + + @Override + public List findElementsByIosUIAutomation(String using) { + return findElements("-ios uiautomation", using); + } + + @Override + public WebElement findElementByAndroidUIAutomator(String using) { + return findElement("-android uiautomator", using); + } + + @Override + public List findElementsByAndroidUIAutomator(String using) { + return findElements("-android uiautomator", using); + } + + @Override + public WebElement findElementByAccessibilityId(String using) { + return findElement("accessibility id", using); + } + + @Override + public List findElementsByAccessibilityId(String using) { + return findElements("accessibility id", using); + } + + @Override + public Location location() { + return locationContext.location(); + } + + @Override + public void setLocation(Location location) { + locationContext.setLocation(location); + } + + private TouchAction createTap(WebElement element, int duration) { + TouchAction tap = new TouchAction(this); + return tap.press(element).waitAction(duration).release(); + } + + private TouchAction createTap(int x, int y, int duration) { + TouchAction tap = new TouchAction(this); + return tap.press(x, y).waitAction(duration).release(); + } + + private static CommandInfo getC(String url) { + return new CommandInfo(url, HttpMethod.GET); + } + + private static CommandInfo postC(String url) { + return new CommandInfo(url, HttpMethod.POST); + } + + @SuppressWarnings("unused") + private static CommandInfo deleteC(String url) { + return new CommandInfo(url, HttpMethod.DELETE); + } + + public URL getRemoteAddress() { + return remoteAddress; + } } \ No newline at end of file diff --git a/src/main/java/io/appium/java_client/PerformsTouchActions.java b/src/main/java/io/appium/java_client/PerformsTouchActions.java index c0d6e7791..f32323dba 100644 --- a/src/main/java/io/appium/java_client/PerformsTouchActions.java +++ b/src/main/java/io/appium/java_client/PerformsTouchActions.java @@ -1,7 +1,31 @@ package io.appium.java_client; public interface PerformsTouchActions { + /** + * Performs a chain of touch actions, which together can be considered an + * entire gesture. See the Webriver 3 spec + * https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html + * + * It's more convenient to call the perform() method of the TouchAction + * object itself. + * + * @param touchAction + * A TouchAction object, which contains a list of individual + * touch actions to perform + * @return the same touch action object + */ public TouchAction performTouchAction(TouchAction touchAction); + /** + * Performs multiple TouchAction gestures at the same time, to simulate + * multiple fingers/touch inputs. See the Webriver 3 spec + * https://dvcs.w3.org/hg/webdriver/raw-file/default/webdriver-spec.html + * + * It's more convenient to call the perform() method of the MultiTouchAction + * object. + * + * @param multiAction + * the MultiTouchAction object to perform. + */ public void performMultiTouchAction(MultiTouchAction multiAction); } diff --git a/src/main/java/io/appium/java_client/android/AndroidDriver.java b/src/main/java/io/appium/java_client/android/AndroidDriver.java new file mode 100644 index 000000000..d520c432b --- /dev/null +++ b/src/main/java/io/appium/java_client/android/AndroidDriver.java @@ -0,0 +1,240 @@ +package io.appium.java_client.android; + +import static com.google.common.base.Preconditions.checkArgument; +import static io.appium.java_client.MobileCommand.CURRENT_ACTIVITY; +import static io.appium.java_client.MobileCommand.END_TEST_COVERAGE; +import static io.appium.java_client.MobileCommand.GET_NETWORK_CONNECTION; +import static io.appium.java_client.MobileCommand.GET_STRINGS; +import static io.appium.java_client.MobileCommand.IS_LOCKED; +import static io.appium.java_client.MobileCommand.KEY_EVENT; +import static io.appium.java_client.MobileCommand.OPEN_NOTIFICATIONS; +import static io.appium.java_client.MobileCommand.PUSH_FILE; +import static io.appium.java_client.MobileCommand.SET_NETWORK_CONNECTION; +import static io.appium.java_client.MobileCommand.START_ACTIVITY; +import static io.appium.java_client.remote.MobileCapabilityType.APP_ACTIVITY; +import static io.appium.java_client.remote.MobileCapabilityType.APP_PACKAGE; +import static io.appium.java_client.remote.MobileCapabilityType.APP_WAIT_ACTIVITY; +import static io.appium.java_client.remote.MobileCapabilityType.APP_WAIT_PACKAGE; +import io.appium.java_client.AndroidKeyCode; +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.AppiumSetting; +import io.appium.java_client.NetworkConnectionSetting; +import io.appium.java_client.remote.MobilePlatform; + +import java.net.URL; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.remote.Response; + +import com.google.common.collect.ImmutableMap; + +public class AndroidDriver extends AppiumDriver implements + AndroidDeviceActionShortcuts, HasAppStrings, HasNetworkConnection, PushesFiles, + StartsActivity { + + private static final String ANDROID_PLATFORM = MobilePlatform.ANDROID; + + private final String METASTATE_PARAM = "metastate"; + private final String LANGUAGE_PARAM = "language"; + private final String CONNECTION_NAME_PARAM = "name"; + private final String CONNECTION_PARAM_PARAM = "parameters"; + private final String DATA_PARAM = "data"; + private final String INTENT_PARAM = "intent"; + + private final String CONNECTION_NAME_VALUE = "network_connection"; + + public AndroidDriver(URL remoteAddress, Capabilities desiredCapabilities) { + super(remoteAddress, substituteMobilePlatform(desiredCapabilities, + ANDROID_PLATFORM)); + } + + /** + * @param key + * code for the key pressed on the Android device + * @param metastate + * metastate for the keypress + * + * @see AndroidKeyCode + * @see AndroidKeyMetastate + * @see AndroidDeviceActionShortcuts#sendKeyEvent(int, Integer) + */ + @Override + public void sendKeyEvent(int key, Integer metastate) { + String[] parameters = new String[] { KEY_CODE, METASTATE_PARAM }; + Object[] values = new Object[] { key, metastate }; + execute(KEY_EVENT, getCommandImmutableMap(parameters, values)); + } + + /** + * @see HasAppStrings#getAppStrings() + */ + @Override + public String getAppStrings() { + Response response = execute(GET_STRINGS); + return response.getValue().toString(); + } + + /** + * @param language + * strings language code + * @return a string of all the localized strings defined in the app + * + * @see HasAppStrings#getAppStrings(String) + */ + @Override + public String getAppStrings(String language) { + Response response = execute(GET_STRINGS, + getCommandImmutableMap(LANGUAGE_PARAM, language)); + return response.getValue().toString(); + } + + /** + * @see HasNetworkConnection#getNetworkConnection() + */ + @Override + public NetworkConnectionSetting getNetworkConnection() { + Response response = execute(GET_NETWORK_CONNECTION); + return new NetworkConnectionSetting(Integer.parseInt(response + .getValue().toString())); + } + + /** + * @param connection + * The NetworkConnectionSetting configuration to use for the + * device + * + * @see HasNetworkConnection#setNetworkConnection(NetworkConnectionSetting) + */ + @Override + public void setNetworkConnection(NetworkConnectionSetting connection) { + // the new version of the webdriver protocol is going forward with + // sending JSON message which look like + // {name: "name of endpoint", parameters: "JSON parameters"} + // this is for webdrivers which run on protocols besides HTTP (like TCP) + // we're implementing that pattern here, for this new method, but + // haven't translated it to all other commands yet + String[] parameters = new String[] { CONNECTION_NAME_PARAM, + CONNECTION_PARAM_PARAM }; + Object[] values = new Object[] { CONNECTION_NAME_VALUE, + ImmutableMap.of("type", connection.value) }; + execute(SET_NETWORK_CONNECTION, + getCommandImmutableMap(parameters, values)); + } + + /** + * @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 + * @see PushesFiles#pushFile(String, byte[]) + */ + @Override + public void pushFile(String remotePath, byte[] base64Data) { + String[] parameters = new String[] { PATH, DATA_PARAM }; + Object[] values = new Object[] { remotePath, base64Data }; + execute(PUSH_FILE, getCommandImmutableMap(parameters, values)); + } + + /** + * @param appPackage + * The package containing the activity. [Required] + * @param appActivity + * The activity to start. [Required] + * @param appWaitPackage + * Automation will begin after this package starts. [Optional] + * @param appWaitActivity + * Automation will begin after this activity starts. [Optional] + * @example driver.startActivity("com.foo.bar", ".MyActivity", null, null); + * + * @see StartsActivity#startActivity(String, String, String, String) + */ + public void startActivity(String appPackage, String appActivity, + String appWaitPackage, String appWaitActivity) + throws IllegalArgumentException { + + checkArgument( + (_isNotNullOrEmpty(appPackage) && _isNotNullOrEmpty(appActivity)), + String.format("'%s' and '%s' are required.", APP_PACKAGE, + APP_ACTIVITY)); + + appWaitPackage = _isNotNullOrEmpty(appWaitPackage) ? appWaitPackage + : ""; + appWaitActivity = _isNotNullOrEmpty(appWaitActivity) ? appWaitActivity + : ""; + + ImmutableMap parameters = ImmutableMap.of(APP_PACKAGE, + appPackage, APP_ACTIVITY, appActivity, APP_WAIT_PACKAGE, + appWaitPackage, APP_WAIT_ACTIVITY, appWaitActivity); + + execute(START_ACTIVITY, parameters); + } + + /** + * @param appPackage The package containing the activity. [Required] + * @param appActivity The activity to start. [Required] + * @example + * *.startActivity("com.foo.bar", ".MyActivity"); + * @see StartsActivity#startActivity(String, String) + */ + @Override + public void startActivity(String appPackage, String appActivity) + throws IllegalArgumentException { + this.startActivity(appPackage, appActivity, null, null); + } + + /** + * Get test-coverage data + * + * @param intent + * intent to broadcast + * @param path + * path to .ec file + */ + public void endTestCoverage(String intent, String path) { + String[] parameters = new String[] { INTENT_PARAM, PATH }; + Object[] values = new Object[] { intent, path }; + execute(END_TEST_COVERAGE, getCommandImmutableMap(parameters, values)); + } + + /** + * Get the current activity being run on the mobile device + */ + public String currentActivity() { + Response response = execute(CURRENT_ACTIVITY); + return response.getValue().toString(); + } + + /** + * Open the notification shade, on Android devices. + */ + public void openNotifications() { + execute(OPEN_NOTIFICATIONS); + } + + /** + * Check if the device is locked. + * + * @return true if device is locked. False otherwise + */ + public boolean isLocked() { + Response response = execute(IS_LOCKED); + return Boolean.parseBoolean(response.getValue().toString()); + } + + /** + * Set the `ignoreUnimportantViews` setting. *Android-only method* + * + * Sets whether Android devices should use `setCompressedLayoutHeirarchy()` + * which ignores all views which are marked IMPORTANT_FOR_ACCESSIBILITY_NO + * or IMPORTANT_FOR_ACCESSIBILITY_AUTO (and have been deemed not important + * by the system), in an attempt to make things less confusing or faster. + * + * @param compress + * ignores unimportant views if true, doesn't ignore otherwise. + */ + // Should be moved to the subclass + public void ignoreUnimportantViews(Boolean compress) { + setSetting(AppiumSetting.IGNORE_UNIMPORTANT_VIEWS, compress); + } + +} diff --git a/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java b/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java index db2c285b8..b051e952c 100644 --- a/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java +++ b/src/test/java/io/appium/java_client/MobileDriverAndroidTest.java @@ -17,9 +17,9 @@ package io.appium.java_client; +import io.appium.java_client.android.AndroidDriver; import io.appium.java_client.android.AndroidKeyMetastate; import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.remote.MobilePlatform; import org.apache.commons.codec.binary.Base64; import org.junit.After; @@ -37,7 +37,7 @@ */ public class MobileDriverAndroidTest { - private AppiumDriver driver; + private AndroidDriver driver; @Before public void setup() throws Exception { @@ -46,10 +46,10 @@ public void setup() throws Exception { DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "Android Emulator"); - capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); + //capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.ANDROID); capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); capabilities.setCapability(MobileCapabilityType.NEW_COMMAND_TIMEOUT, 120); - driver = new AppiumDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities); + driver = new AndroidDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities); } @After From c158b009da60c4d3b4fd8d1eff3ebfafe65d5f4f Mon Sep 17 00:00:00 2001 From: Sergey Tikhomirov Date: Sun, 21 Sep 2014 15:17:20 +0400 Subject: [PATCH 3/3] #105 & #108 IOSDriver. AppiumDriver. InteractsWithApps. --- .../io/appium/java_client/AppiumDriver.java | 112 +++--------------- .../appium/java_client/InteractsWithApps.java | 54 +++++++++ .../io/appium/java_client/ios/IOSDriver.java | 74 ++++++++++++ .../java_client/MobileDriverIOSTest.java | 8 +- 4 files changed, 147 insertions(+), 101 deletions(-) create mode 100644 src/main/java/io/appium/java_client/InteractsWithApps.java create mode 100644 src/main/java/io/appium/java_client/ios/IOSDriver.java diff --git a/src/main/java/io/appium/java_client/AppiumDriver.java b/src/main/java/io/appium/java_client/AppiumDriver.java index 16479369b..38518748e 100644 --- a/src/main/java/io/appium/java_client/AppiumDriver.java +++ b/src/main/java/io/appium/java_client/AppiumDriver.java @@ -86,7 +86,7 @@ public class AppiumDriver extends RemoteWebDriver implements MobileDriver, ContextAware, Rotatable, FindsByIosUIAutomation, FindsByAndroidUIAutomator, FindsByAccessibilityId, LocationContext, - DeviceActionShortcuts, TouchShortcuts, InteractsWithFiles { + DeviceActionShortcuts, TouchShortcuts, InteractsWithFiles, InteractsWithApps { private final static ErrorHandler errorHandler = new ErrorHandler( new ErrorCodesMobile(), true); @@ -231,8 +231,9 @@ public ExecuteMethod getExecuteMethod() { } /** - * Reset the currently running app for this session + * @see InteractsWithApps#resetApp() */ + @Override public void resetApp() { execute(MobileCommand.RESET); } @@ -293,48 +294,9 @@ public void hideKeyboard() { } /** - * Hides the keyboard if it is showing. Available strategies are PRESS_KEY - * and TAP_OUTSIDE. One taps outside the keyboard, the other presses a key - * of your choosing (probably the 'Done' key). Hiding the keyboard often - * depends on the way an app is implemented, no single strategy always - * works. - * - * These parameters are only for iOS, and ignored by Android. - * - * @param strategy - * HideKeyboardStrategy - * @param keyName - * a String, representing the text displayed on the button of the - * keyboard you want to press. For example: "Done" - */ - //Should be moved to the subclass - public void hideKeyboard(String strategy, String keyName) { - String[] parameters = new String[] { "strategy", "key" }; - Object[] values = new Object[] { strategy, keyName }; - execute(HIDE_KEYBOARD, getCommandImmutableMap(parameters, values)); - } - - /** - * Hides the keyboard by pressing the button specified by keyName if it is - * showing. This is an iOS only command. - * - * @param keyName - * The button pressed by the mobile driver to attempt hiding the - * keyboard - */ - //Should be moved to the subclass - public void hideKeyboard(String keyName) { - execute(HIDE_KEYBOARD, ImmutableMap.of("keyName", keyName)); - } - - /** - * Runs the current app as a background app for the number of seconds - * requested. This is a synchronous method, it returns after the back has - * been returned to the foreground. - * - * @param seconds - * Number of seconds to run App in background + * @see InteractsWithApps#runAppInBackground(int) */ + @Override public void runAppInBackground(int seconds) { execute(RUN_APP_IN_BACKGROUND, ImmutableMap.of("seconds", seconds)); } @@ -487,31 +449,9 @@ public void zoom(int x, int y) { } /** - * In iOS apps, named TextFields have the same accessibility Id as their - * containing TableElement. This is a convenience method for getting the - * named TextField, rather than its containing element. - * - * @param name - * accessiblity id of TextField - * @return The textfield with the given accessibility id - */ - //Should be moved to the subclass - public WebElement getNamedTextField(String name) { - MobileElement element = (MobileElement) findElementByAccessibilityId(name); - if (element.getTagName() != "TextField") { - return element.findElementByAccessibilityId(name); - } - - return element; - } - - /** - * Checks if an app is installed on the device - * - * @param bundleId - * bundleId of the app - * @return True if app is installed, false otherwise + * @see InteractsWithApps#isAppInstalled(String) */ + @Override public boolean isAppInstalled(String bundleId) { Response response = execute(IS_APP_INSTALLED, ImmutableMap.of("bundleId", bundleId)); @@ -520,59 +460,37 @@ public boolean isAppInstalled(String bundleId) { } /** - * Install an app on the mobile device - * - * @param appPath - * path to app to install + * @see InteractsWithApps#installApp(String) */ + @Override public void installApp(String appPath) { execute(INSTALL_APP, ImmutableMap.of("appPath", appPath)); } /** - * Remove the specified app from the device (uninstall) - * - * @param bundleId - * the bunble identifier (or app id) of the app to remove + * @see InteractsWithApps#removeApp(String) */ + @Override public void removeApp(String bundleId) { execute(REMOVE_APP, ImmutableMap.of("bundleId", bundleId)); } /** - * Launch the app which was provided in the capabilities at session creation + * @see InteractsWithApps#launchApp() */ + @Override public void launchApp() { execute(LAUNCH_APP); } /** - * Close the app which was provided in the capabilities at session creation + * @see InteractsWithApps#closeApp() */ + @Override public void closeApp() { execute(CLOSE_APP); } - /** - * Lock the device (bring it to the lock screen) for a given number of - * seconds - * - * @param seconds - * number of seconds to lock the screen for - */ - //Should be moved to the subclass (supposed to be in iOS) - public void lockScreen(int seconds) { - execute(LOCK, ImmutableMap.of("seconds", seconds)); - } - - /** - * Simulate shaking the device This is an iOS-only method - */ - //Should be moved to the subclass - public void shake() { - execute(SHAKE); - } - /** * Get settings stored for this test session It's probably better to use a * convenience function, rather than use this function directly. Try finding diff --git a/src/main/java/io/appium/java_client/InteractsWithApps.java b/src/main/java/io/appium/java_client/InteractsWithApps.java new file mode 100644 index 000000000..59f6d0566 --- /dev/null +++ b/src/main/java/io/appium/java_client/InteractsWithApps.java @@ -0,0 +1,54 @@ +package io.appium.java_client; + +public interface InteractsWithApps { + /** + * Launch the app which was provided in the capabilities at session creation + */ + public void launchApp(); + + /** + * Install an app on the mobile device + * + * @param appPath + * path to app to install + */ + public void installApp(String appPath); + + /** + * Checks if an app is installed on the device + * + * @param bundleId + * bundleId of the app + * @return True if app is installed, false otherwise + */ + public boolean isAppInstalled(String bundleId); + + /** + * Reset the currently running app for this session + */ + public void resetApp(); + + /** + * Runs the current app as a background app for the number of seconds + * requested. This is a synchronous method, it returns after the back has + * been returned to the foreground. + * + * @param seconds + * Number of seconds to run App in background + */ + public void runAppInBackground(int seconds); + + /** + * Remove the specified app from the device (uninstall) + * + * @param bundleId + * the bunble identifier (or app id) of the app to remove + */ + public void removeApp(String bundleId); + + /** + * Close the app which was provided in the capabilities at session creation + */ + public void closeApp(); + +} diff --git a/src/main/java/io/appium/java_client/ios/IOSDriver.java b/src/main/java/io/appium/java_client/ios/IOSDriver.java new file mode 100644 index 000000000..a88bd39b6 --- /dev/null +++ b/src/main/java/io/appium/java_client/ios/IOSDriver.java @@ -0,0 +1,74 @@ +package io.appium.java_client.ios; + +import static io.appium.java_client.MobileCommand.HIDE_KEYBOARD; +import static io.appium.java_client.MobileCommand.LOCK; +import static io.appium.java_client.MobileCommand.SHAKE; + +import java.net.URL; + +import org.openqa.selenium.Capabilities; +import org.openqa.selenium.WebElement; + +import com.google.common.collect.ImmutableMap; + +import io.appium.java_client.AppiumDriver; +import io.appium.java_client.MobileElement; +import io.appium.java_client.remote.MobilePlatform; + +public class IOSDriver extends AppiumDriver implements IOSDeviceActionShortcuts, GetsNamedTextField{ + private static final String IOS_PLATFORM = MobilePlatform.IOS; + + public IOSDriver(URL remoteAddress, Capabilities desiredCapabilities) { + super(remoteAddress, substituteMobilePlatform(desiredCapabilities, + IOS_PLATFORM)); + } + + /** + * @see IOSDeviceActionShortcuts#hideKeyboard(String, String) + */ + @Override + public void hideKeyboard(String strategy, String keyName) { + String[] parameters = new String[] { "strategy", "key" }; + Object[] values = new Object[] { strategy, keyName }; + execute(HIDE_KEYBOARD, getCommandImmutableMap(parameters, values)); + } + + /** + * @see IOSDeviceActionShortcuts#hideKeyboard(String) + */ + @Override + public void hideKeyboard(String keyName) { + execute(HIDE_KEYBOARD, ImmutableMap.of("keyName", keyName)); + } + + /** + * @see IOSDeviceActionShortcuts#shake() + */ + @Override + public void shake() { + execute(SHAKE); + } + + /** + * @see GetsNamedTextField#getNamedTextField(String) + */ + @Override + public WebElement getNamedTextField(String name) { + MobileElement element = (MobileElement) findElementByAccessibilityId(name); + if (element.getTagName() != "TextField") { + return element.findElementByAccessibilityId(name); + } + return element; + } + + /** + * Lock the device (bring it to the lock screen) for a given number of + * seconds + * + * @param seconds + * number of seconds to lock the screen for + */ + public void lockScreen(int seconds) { + execute(LOCK, ImmutableMap.of("seconds", seconds)); + } +} diff --git a/src/test/java/io/appium/java_client/MobileDriverIOSTest.java b/src/test/java/io/appium/java_client/MobileDriverIOSTest.java index 0256576a4..b34ff2cf2 100644 --- a/src/test/java/io/appium/java_client/MobileDriverIOSTest.java +++ b/src/test/java/io/appium/java_client/MobileDriverIOSTest.java @@ -17,9 +17,9 @@ package io.appium.java_client; +import io.appium.java_client.ios.IOSDriver; import io.appium.java_client.remote.HideKeyboardStrategy; import io.appium.java_client.remote.MobileCapabilityType; -import io.appium.java_client.remote.MobilePlatform; import org.junit.After; import org.junit.Before; @@ -38,7 +38,7 @@ */ public class MobileDriverIOSTest { - private AppiumDriver driver; + private IOSDriver driver; @Before public void setup() throws Exception { @@ -47,10 +47,10 @@ public void setup() throws Exception { DesiredCapabilities capabilities = new DesiredCapabilities(); capabilities.setCapability(MobileCapabilityType.BROWSER_NAME, ""); capabilities.setCapability(MobileCapabilityType.PLATFORM_VERSION, "7.1"); - capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); + //capabilities.setCapability(MobileCapabilityType.PLATFORM_NAME, MobilePlatform.IOS); capabilities.setCapability(MobileCapabilityType.DEVICE_NAME, "iPhone Simulator"); capabilities.setCapability(MobileCapabilityType.APP, app.getAbsolutePath()); - driver = new AppiumDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities); + driver = new IOSDriver(new URL("http://127.0.0.1:4723/wd/hub"), capabilities); } @After