Appium 解决格式化字符输入的困扰--Android

最后一次被盗 · 2014年10月25日 · 最后由 kuroky 回复于 2015年06月19日 · 2170 次阅读
本帖已被设为精华帖!

项目需求:
我们有一个输入框,为了提高用户体验,会自动格式化输入的字符串,其实这些东西在各种电商 APP 上很常见,举个例子,
例如输入手机号:13323450000
输入过程中,APP 会自动做判断,根据输入的长度做判断进而格式化例如变为:133 2345 0000
这期间的实现其实很简单,就是每当我们输入字符时,都会触发一个 Event 事件,如果当我们输入 3 个字符后,在输入第四个字符时,程序就会自动在其前面增加一个空格。
如果你们试验一下的话就是会发现,如果我们用 sendkeys 来输入的话,基本很难成功,输入的字符不是多几位就是少几位,反正就是各种错误,后来我想到是否可以分开输入,3,4,4 节奏的输入,但是还是会失败,为什么呢?
下面贴一段代码,大家一看便知:

package io.appium.android.bootstrap.handler;

import com.android.uiautomator.core.UiDevice;
import com.android.uiautomator.core.UiObjectNotFoundException;
import io.appium.android.bootstrap.*;
import org.json.JSONException;

import java.util.Hashtable;

/**
 * This handler is used to set text in elements that support it.
 *
 */
public class SetText extends CommandHandler {

  /*
   * @param command The {@link AndroidCommand} used for this handler.
   *
   * @return {@link AndroidCommandResult}
   *
   * @throws JSONException
   *
   * @see io.appium.android.bootstrap.CommandHandler#execute(io.appium.android.
   * bootstrap.AndroidCommand)
   */
  @Override
  public AndroidCommandResult execute(final AndroidCommand command)
      throws JSONException {
    if (command.isElementCommand()) {
      // Only makes sense on an element
      try {
        final Hashtable<String, Object> params = command.params();
        final AndroidElement el = command.getElement();
        boolean replace = Boolean.parseBoolean(params.get("replace").toString());
        String text = params.get("text").toString();
        boolean pressEnter = false;
        if (text.endsWith("\\n")) {
          pressEnter = true;
          text = text.replace("\\n", "");
          Logger.debug("Will press enter after setting text");
        }
        boolean unicodeKeyboard = false;
        if (params.get("unicodeKeyboard") != null) {
          unicodeKeyboard = Boolean.parseBoolean(params.get("unicodeKeyboard").toString());
        }
        String currText = el.getText();
        new Clear().execute(command);
        if (!el.getText().isEmpty()) {
          // clear could have failed, or we could have a hint in the field
          // we'll assume it is the latter
          Logger.debug("Text not cleared. Assuming remainder is hint text.");
          currText = "";
        }
        if (!replace) {
          text = currText + text;
        }
        final boolean result = el.setText(text, unicodeKeyboard);
        if (pressEnter) {
          final UiDevice d = UiDevice.getInstance();
          d.pressEnter();
        }
        return getSuccessResult(result);
      } catch (final UiObjectNotFoundException e) {
        return new AndroidCommandResult(WDStatus.NO_SUCH_ELEMENT,
            e.getMessage());
      } catch (final Exception e) { // handle NullPointerException
        return getErrorResult("Unknown error");
      }
    } else {
      return getErrorResult("Unable to set text without an element.");
    }
  }
}

这就是 bootstrap 中的 setText 类,中间有一段代码 “new Clear().execute(command);” 清除命令
Appium 的实现都是通过 bootstrap 来和 uiautomator 做交互的,So 刚刚那个想法有点儿障碍,但是我没有屈服,我们是开源的所以我修改了这个类,但是实际操作后我发现貌似没有什么作用,在赋值时依然会先 clear,这是为什么呢,没办法,再次向下追踪,刚刚已经说过,bootstrap 是跟 uiautomator 做交互,实际上也就是说是最终通过 uiautomator 来操作我们的手机,所以我找到了 uiautomator 的源码,发现了一个我不愿意相信的事实,大家看下面的源码,是 UiObject 类的内容,截图了:

这个里面也会 clear,so,我彻底死心了这个办法是无解了。
不过我们不会屈服的,对吧,哈哈,换个角度,如果我能够控制输入的速度,是不是就可以了呢,可是,Appium 暴露给我们的只有一个 sendKeys 的 API,怎么办呢?
继续解读 bootstrap 的源码,通过 UiObject 类中的那段代码我们可以看到,最终调用的是一个 sendText 的 API,继续往下挖,我们会发现这个 API 无法再往下点了(通过 ctrl + 单击),说明是隐藏的类,从 Android sdk 的 source 包中,我找到了这个方法的源码:

public boolean sendText(String text) {
        if (DEBUG) {
            Log.d(LOG_TAG, "sendText (" + text + ")");
        }

        KeyEvent[] events = mKeyCharacterMap.getEvents(text.toCharArray());

        if (events != null) {
            long keyDelay = Configurator.getInstance().getKeyInjectionDelay();
            for (KeyEvent event2 : events) {
                // We have to change the time of an event before injecting it because
                // all KeyEvents returned by KeyCharacterMap.getEvents() have the same
                // time stamp and the system rejects too old events. Hence, it is
                // possible for an event to become stale before it is injected if it
                // takes too long to inject the preceding ones.
                KeyEvent event = KeyEvent.changeTimeRepeat(event2,
                        SystemClock.uptimeMillis(), 0);
                if (!injectEventSync(event)) {
                    return false;
                }
                SystemClock.sleep(keyDelay);
            }
        }
        return true;
    }

这里面我们发现了什么?对,有 sleep,哈哈,好像看到希望了,先简单说一下它的工作原理,我们把一个 string 给这个 API,它先把 String 分割为单个的 KeyEvent,然后有一个 for 循环来依次执行,每次执行都会判断一下是否成功,并且会 sleep 一下。所以,我们控制一下这个 keyDelay 就能实现我们的愿望了,好激动,不过这个 keyDelay 是传人的,而是
“long keyDelay = Configurator.getInstance().getKeyInjectionDelay();”
那么我们需要继续深挖了,Configurator 类源码不少,我贴几个重要的如下:
getKeyInjectionDelay API:

/**
     * Gets the current delay between key presses when injecting text input.
     * See {@link UiObject#setText(String)}
     *
     * @return current delay in milliseconds
     * @since API Level 18
     */
    public long getKeyInjectionDelay() {
        return mKeyInjectionDelay;
    }

getKeyInjectionDelay API:

/**
     * Sets a delay between key presses when injecting text input.
     * See {@link UiObject#setText(String)}
     *
     * @param delay Delay value in milliseconds
     * @return self
     * @since API Level 18
     */
    public Configurator setKeyInjectionDelay(long delay) {
        mKeyInjectionDelay = delay;
        return this;
    }

mKeyInjectionDelay 成员变量

// Default is inject as fast as we can
    private long mKeyInjectionDelay = 0; // ms

getInstance()

/**
     * Retrieves a singleton instance of Configurator.
     *
     * @return Configurator instance
     * @since API Level 18
     */
    public static Configurator getInstance() {
        if (sConfigurator == null) {
            sConfigurator = new Configurator();
        }
        return sConfigurator;
    }

行了,这样大家应该可以看出来了,getInstance() 可以获取一个实例,所以,我们可以在 bootstrap 做一些修改,找到我最上面贴的那段代码中加一行这样的代码:
Configurator.getInstance().setKeyInjectionDelay(500);
位置是在这行代码前面:
final boolean result = el.setText(text, unicodeKeyboard);
新增代码的意思就是将 mKeyInjectionDelay 设置为 500 毫秒,即:在每次执行 KeyEvent 时先等待 500 毫秒。
结果:我发现这个做法成功的几率也很低,因为它会拖慢整个 Event 执行的速度,只有在机器变的很慢时才会有效,当时我用的是红米手机,刷了 4.4 的原生 Android 系统的,so,这个办法也行不通,不过确让我了解了 uiautomator 的运行原理及部分源码的运行逻辑,虽然时间浪费了,不过还是值得的。
但是,问题没有解决,怎么办?
后来我再次去研究 Appium 提供的 API,发现了一个 sendKeyEvent 的 API,这次好像就没有那么大的激情了,不过只能死马当活马医了,这个方法是直接发送 key 值,关于 key 值,key 值的 Map 网上有很多,在这里我给大家直接发一个源码文件吧,就不贴了,而且咱们论坛里已经有人发出来了,大家也可以去参考一下。
实现方法我大体说一下,其实很简单,先把 String 分割为单个的 char,然后通过 key 的 Map 映射关系一一转换为 key 值,然后使用一个 for 循环,一一直接调用 sendKeyEvent 方法把 key 值传进去即可,中间设置一个 sleep,我设置的是 500 毫秒,失败的几率很小,目前我的公司项目中是这么用的,没什么大问题,我把它封装成了一个方法,在某种意义上可以代替 sendKeys 方法了。

最后,关于这个问题,我首先是从 github 上咨询的官方,但是官方一直没有什么具体的解决办法!
https://github.com/appium/appium/issues/3812#issuecomment-60433195
关于 keyEvent 映射,贴一个本站的帖子:
http://www.testerhome.com/topics/1386
当然如果有 AndroidSDK 源码的童鞋也可以去找一下这个类:KeyEvent.java(这是最权威的!)

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 5 条回复 时间 点赞
匿名 #3 · 2014年10月27日

楼主的文采可谓激情四射,文采飞扬啊,拜读完毕!
为啥增加 sleep 就可以解决呢?

写的很精彩,大半夜我就决定先给你加精华了。
appium 的实现并不完美,有些地方比如输入文本,就会自作聪明的 clear, 所以有些时候是需要调用更底层的办法的。

分析的很精彩,希望能将这些点滴积累起来,形成社区的财富啊

#2 楼 @seveniruby 这个 appium 也是没办法,它想做一个全兼容。就选择了先 clear。

兄弟,这个方法早试过了,貌似还是不稳定

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册