From afa2928ddfe640f7ad78db7a726820a8dbf9a05e Mon Sep 17 00:00:00 2001 From: bootstraponline Date: Fri, 31 Jan 2014 16:14:14 -0500 Subject: [PATCH] Fix long click on Android Android's uiautomator has a broken long click implementation. uiautomator will not pause long enough on long click which causes Android to detect the long click as a normal click. This fix uses reflection to access uiautomator's private touchDown/touchUp methods to reimplement longClick correctly. In theory I should be able to obtain a reference to android.app.UiAutomation without using reflection. However this doesn't seem possible when using uiautomator. getInstrumentation().getUiAutomation(); doesn't work because we're not using instrumentation. --- .../android/bootstrap/AndroidElement.java | 4 + .../bootstrap/handler/TouchLongClick.java | 91 +++++++++++++++++-- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/AndroidElement.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/AndroidElement.java index 6424d6165..98f364b83 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/AndroidElement.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/AndroidElement.java @@ -195,6 +195,10 @@ public class AndroidElement { return el; } + public Rect getVisibleBounds() throws UiObjectNotFoundException { + return el.getVisibleBounds(); + } + public boolean longClick() throws UiObjectNotFoundException { return el.longClick(); } diff --git a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java index 4c790773d..8f1951274 100644 --- a/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java +++ b/lib/devices/android/bootstrap/src/io/appium/android/bootstrap/handler/TouchLongClick.java @@ -1,34 +1,109 @@ package io.appium.android.bootstrap.handler; -import com.android.uiautomator.core.UiObjectNotFoundException; -import io.appium.android.bootstrap.*; +import io.appium.android.bootstrap.AndroidCommand; +import io.appium.android.bootstrap.AndroidCommandResult; +import io.appium.android.bootstrap.AndroidElement; +import io.appium.android.bootstrap.CommandHandler; +import io.appium.android.bootstrap.Logger; +import io.appium.android.bootstrap.WDStatus; import io.appium.android.bootstrap.exceptions.ElementNotInHashException; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + import org.json.JSONException; +import android.graphics.Rect; +import android.os.SystemClock; + +import com.android.uiautomator.core.UiDevice; +import com.android.uiautomator.core.UiObjectNotFoundException; + /** * This handler is used to long click elements in the Android UI. - * + * */ public class TouchLongClick extends CommandHandler { + private static Field enableField(final Class clazz, final String field) + throws SecurityException, NoSuchFieldException { + Logger.debug("Updating class \"" + clazz + "\" to enable field \"" + field + + "\""); + final Field fieldObject = clazz.getDeclaredField(field); + fieldObject.setAccessible(true); + return fieldObject; + } + + /* + * UiAutomator has a broken longClick. getAutomatorBridge is private so we + * access the bridge via reflection to use the touchDown / touchUp methods. + */ + private boolean correctLongClick(final int x, final int y) { + try { + /* + * bridge.getClass() returns ShellUiAutomatorBridge on API 18/19 so use + * the super class. + */ + + final UiDevice device = UiDevice.getInstance(); + final Object bridge = enableField(device.getClass(), + "mUiAutomationBridge").get(device); + final Object controller = enableField(bridge.getClass().getSuperclass(), + "mInteractionController").get(bridge); + final Class controllerClass = controller.getClass(); + + Logger.debug("Finding methods on class: " + controllerClass); + final Method touchDown = controllerClass.getDeclaredMethod("touchDown", + int.class, int.class); + touchDown.setAccessible(true); + final Method touchUp = controllerClass.getDeclaredMethod("touchUp", + int.class, int.class); + touchUp.setAccessible(true); + + if ((Boolean) touchDown.invoke(controller, x, y)) { + SystemClock.sleep(2000); + if ((Boolean) touchUp.invoke(controller, x, y)) { + return true; + } + } + return false; + + } catch (final Exception e) { + Logger.debug("Problem invoking correct long click: " + e); + return false; + } + } + /** - * - * @param command The {@link AndroidCommand} + * + * @param command + * The {@link AndroidCommand} * @return {@link AndroidCommandResult} * @throws JSONException * @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.bootstrap.AndroidCommand) */ @Override - public AndroidCommandResult execute(AndroidCommand command) throws JSONException { + public AndroidCommandResult execute(final AndroidCommand command) + throws JSONException { if (!command.isElementCommand()) { return getErrorResult("Unable to long click without an element."); } try { final AndroidElement el = command.getElement(); + + final Rect bounds = el.getVisibleBounds(); + final int x = bounds.centerX(); + final int y = bounds.centerY(); + + if (correctLongClick(x, y)) { + return getSuccessResult(true); + } + + Logger.debug("Falling back to broken longClick"); + final boolean res = el.longClick(); return getSuccessResult(res); - } catch (final UiObjectNotFoundException e) { return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT, e.getMessage()); } catch (final ElementNotInHashException e) { @@ -37,4 +112,4 @@ public class TouchLongClick extends CommandHandler { return getErrorResult(e.getMessage()); } } -} +} \ No newline at end of file