Android手机上用户操作模拟方法的研究与实现

时间:2022-04-26
本文章向大家介绍Android手机上用户操作模拟方法的研究与实现,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、 问题背景

最近研究了一下Android手机上用户操作的模拟方法, 有一些心得与大家分享下。

之所以去研究Android手机上用户操作的模拟方法,是因为最近做毕业设计,想尝试开发Android的UI自动化测试。最开始使用MonkeyRunner来录制脚本,开发过程中发现在MonkeyRunner上录制时,模拟拖拽的操作不方便。

接着我又尝试自己通过Monkey中的同样的方法进行用户操作的模拟,结果运行的时候出了Injecting to another application requires INJECT_EVENTS permission异常,甚至是加了INJECT_EVENTS权限也报同样的错误。在网上查询后才知道Android系统2.0版本以前并没有对跨进程间发送事件进行限制,之后的版本从安全方面考虑加上了限制。Monkey之所以不会出这个异常,是因为它与系统一起发布,拥有和系统一样的签名。

因此笔者想是否有其他的方法可以跨进程模拟用户操作,且还能在不同的手机上适用。

二、 目前现状

经过一段时间学习,笔者发现目前大部分的Android自动化测试工具使用两种方法来模拟用户操作,一是通过Monkey或者Monkey同样的方法来完成,另外一种是通过读写Android系统的输入输入设备节点文件来完成。

首先看下Monkey是如何实现用户模拟的。

Android2.3.7中的Monkey实现了三种途径向系统插入Input事件:

1. 随机生成Input事件,对应类为MonkeySourceRandom;

2. 从脚本读取Input事件,对应类为MonkeySourceScript ;

3. 从socket读取Input事件,对应类为MonkeySourceNetwork ;

这三个类均继承自MonkeyEventSource类,它们将从不同源获取到的event放进内部queue,在Monkey的runMonkeyCycles()循环中取出这些event插入系统。如下面的代码所示:

MonkeyEvent ev = mEventSource.getNextEvent();            
if (ev != null) {
int injectCode = ev.injectEvent(mWm, mAm, mVerbose);  
}

MonkeyEvent模拟系统里面的Input Event有MonkeyKeyEvent、MonkeyMotionEvent、MonkeyPowerEvent、MonkeyNoopEvent、MonkeyFlipEvent、MonkeyCommandEvent、MonkeyActivityEvent。每个Event都实现了 injectEvent()函数,每种事件根据类型实现了具体的事件插入。 例如 MonkeyCommandEvent是执行一个命令,代码如下:

    public int injectEvent(IWindowManager iwm, IActivityManager iam, int verbose)
{        
if (mCmd != null)
{
//Execute the shell command            
try {                
java.lang.Process p = Runtime.getRuntime().exec(mCmd);
int status = p.waitFor();
System.err.println("// Shell command " + mCmd + " status was " + status);
} catch (Exception e) {
System.err.println("// Exception from " + mCmd + ":");
System.err.println(e.toString());
}        
}      
return MonkeyEvent.INJECT_SUCCESS;    
       }[1]

第二方法是通过读写Android系统的输入设备节点文件来完成。从网上可以找到较多的文档描述如何获取和模拟用户的操作,但是没有很好的普遍适用的代码实现。该方法主要是基于Android的输入输出子系统。先下Android的Input子系统是如何工作的。

Android系统本质上是Linux系统,在Linux中输入子系统是由输入子系统设备驱动层、输入子系统核心层(Input Core)和输入子系统事件处理层(Event Handler)组成。其中设备驱动层提供对硬件各寄存器的读写访问和将底层硬件对用户输入访问的响应转换为标准的输入事件,再通过核心层提交给事件处理层;而核心层对下提供了设备驱动层的编程接口,对上又提供了事件处理层的编程接口;而事件处理层就为我们用户空间的应用程序提供了统一访问设备的接口和驱动层提交来的事件处理[2]。如图1所示, 图中的/dev/input/eventX就是所谓输入设备节点文件,它是硬件设备提供给系统的设备接口文件。

图1 Linxu系统的Input子系统示意图

再来看看Linux的系统是如何从设备节点文件获取事件的。系统进程的EventHub会读取输入设备节点文件的事件,而InputReaderThread从EventHub中不断读取事件,并通过pipe传递给InputDispatcherThread,由InputDispatcherThread分发到各应用程序,由它们去处理。如图2所示。

图2 底层按键事件获取的简单流程[3]

如何通过读写设备节点文件来模拟用户操作呢? 以Touch事件为例,在读写之前需要知道触摸屏对应的设备节点文件是哪一个。在Android上我们可以通过命令adb shell cat /proc/bus/input/devices来查看,在笔者三星GT-i9300上的运行结果如图3所示。在这个手机上/dev/input/event2就是触摸屏的设备节点文件。找到了这个设备文件后,可以通过命令adb shell getevent /dev/input/event2读这个设备的事件信息,轻点一下手机屏幕,再查看getevent接收到的数据,结果如图4所示。 Android系统地单击操作其实是由一系列的事件组成的,图中的0003 0035 00000190中0003是EV_ABS的代码,0035是多点触摸的ABS_MT_POSITION_X事件代码,00000190代表坐标的值。 详细的代码定义可以参考Android的input.h的源代码或者访问链接。现在得到了这些事件序列,只要我们以同样的序列通过Android系统的sendevent写到系统中就会触发单击操作了。但是这样还不能达到方便的模拟用户操作,并且上述方法还不具有普适性。

图3 查看Input设备节点文件

图4 三星GT-i9300手机上单击屏幕时触发的事件

三、 研究内容与结果

为了让程序可以在不同的手机上都可以运行,代码需要解决以下两个问题:

1. 由于不同厂家的设备都不一样,所以event的文件也各不相同,程序首先需要找到这个文件。

2. 由于Android使用的touch的协议也不一样,代码必须要很好的兼容不同的手机。

首先需要做的是找到手机中的touch screen对应的event文件。示例代码如下:

for(int i=0; i<EV_MAX; i++)
{
       memset(devicePath, 0, sizeof(devicePath));
       memset(buf, 0, sizeof(buf));
       sprintf(devicePath, "/dev/input/event%d", i);
       //LOGI(LOG_TAG, devicePath);
       if ((fd = open(devicePath, O_RDONLY)) < 0)
       {
              LOGI(LOG_TAG, "Open device failed");
              //打开该文件失败,继续打开下一个
              continue;
       }
       // EVIOCGNAME参考input.h
       ioctl(fd, EVIOCGNAME(sizeof(buf)), buf); 
       //根据设备的名称来查找
       if( strstr(buf, “touchscreen”))
       {
              memcpy(device->deviceName, buf, sizeof(buf));
              //找到了,把event文件的路径保留下来
              memcpy(device->devicePath, devicePath, sizeof(devicePath));、
}
}

其次就是解决对touch协议的支持问题。Android系统2.0以前不支持multi-touch事件,2.0以后都支持了。当multi-touch协议和single-touch协议在手机上被使用时,单击屏幕触发的将是multi-touch协议的坐标,而single-touch协议事件的坐标将被忽略[4];另外multi-touch事件还分为Type A[5]和Type B,所以代码需要对以上情况进行支持。

在我的代码中首先是读取了设备的设置,判断出手机是multi-touch还是single-touch,以及设备支持的事件等。示例代码片段如下,该代码是模仿Android系统的EventHub.cpp写的,大家如果有兴趣可以去看看这个文件。

//*******************
//在这之前用ioctl打开我们找到的 /dev/input/event*/ 文件
//获取ABS的设置信息,并根据ABS信息判断哪些Event是支持的
ioctl(fd, EVIOCGBIT(EV_ABS, sizeof(absBitmask)), absBitmask);
ioctl(fd, EVIOCGBIT(EV_KEY, sizeof(keyBitmask)), keyBitmask);
if( test_bit(ABS_MT_POSITION_X, absBitmask)
&& test_bit(ABS_MT_POSITION_Y, absBitmask))
{
       LOGI(LOG_TAG, "This input device is a multi-touch device");
       device->isMultiTouch = true;
       device->supportMTEvent->bPositionX = true;
       device->supportMTEvent->bPositionY = true;
       //以下开始判断哪些事件是被该设备支持的                  
       if( test_bit(ABS_MT_SLOT, absBitmask))
       {
              device->supportMTEvent->bSlot = true;
              LOGI(LOG_TAG, "ABS_MT_SLOT is supported");
       }
       ……
}else if (test_bit(BTN_TOUCH, keyBitmask)
              && test_bit(ABS_X, absBitmask)
              && test_bit(ABS_Y, absBitmask)){
       LOGI(LOG_TAG, "This input device is a single-touch device");
       device->isSingleTouch = true;
}

目前已经获得了设备支持的协议了,那我们就可以将touch操作封装成函数给各种操作调用了,在我的代码实现了一个函数,它负责根据系统支持的事件来发送对应的事件。示例代码如下:

/***
*     该函数是用于判断某个触摸事件是否支持,如果支持我们就发送它
*     将判断放到这个函数是因为在touch和drag等函数就不用再去判断了
**/
int TouchUtil::sendTouchEvent(const int fd, const int eventId, const int value)
{
       if (fd <= 0)
       {
              return RET_ERROR;
       }
       switch(eventId)
       {
              case ABS_MT_SLOT:
                     if( touchDevice->supportMTEvent->bSlot)
                     {
                            writeEvent(fd, EV_ABS, ABS_MT_SLOT, value);
                     }
                     break;
              case ABS_MT_TOUCH_MAJOR:
                     if( touchDevice->supportMTEvent->bTouchMajor)
                     {
                            writeEvent(fd, EV_ABS, ABS_MT_TOUCH_MAJOR, value);
                     }
           ……
              default:
                     break;
       }
       return RET_OK;
}

此外由于普通的single touch事件基本上可以分成按下手指、移动手指、释放手指的操作。由于在前面的sendTouchEvent函数里面已经对支持的事件做了判断,所以在下面的函数中直接给了touch协议事件的全集。示例代码如下:

       int TouchUtil::down(const int x, const int y, const int slot, const int trackintId)
{
       if(!isDeviceReady)
       {
              return RET_ERROR;
       }
       if( touchDevice->isMultiTouch)
       {
              int value = (rand() % 16) + 1;
              //按下一个点
              sendTouchEvent(fd, ABS_MT_SLOT, slot);
              sendTouchEvent(fd, ABS_MT_TRACKING_ID, trackintId);
              sendTouchEvent(fd, ABS_MT_POSITION_X, x);
              sendTouchEvent(fd, ABS_MT_POSITION_Y, y);
              sendTouchEvent(fd, ABS_MT_TOUCH_MAJOR, value);
              sendTouchEvent(fd, ABS_MT_TOUCH_MINOR, value);
              sendTouchEvent(fd, ABS_MT_WIDTH_MAJOR, value);
              sendTouchEvent(fd, ABS_MT_WIDTH_MINOR, value);
              sendTouchEvent(fd, ABS_MT_ORIENTATION, value);
              sendTouchEvent(fd, ABS_MT_TOOL_TYPE, BTN_TOOL_FINGER);
              sendTouchEvent(fd, ABS_MT_BLOB_ID, value);
              sendTouchEvent(fd, ABS_MT_PRESSURE, value);
              sendTouchEvent(fd, ABS_MT_DISTANCE, value);
              sendTouchEvent(fd, ABS_MT_TOOL_X, value);
              sendTouchEvent(fd, ABS_MT_TOOL_Y, value);
              sendTouchEvent(fd, SYN_MT_REPORT, 0);
              sendTouchEvent(fd, SYN_REPORT, 0);
       }else
       {
              writeEvent(fd, EV_ABS, ABS_X, x);
              writeEvent(fd, EV_ABS, ABS_Y, y);
              writeEvent(fd, EV_ABS, ABS_PRESSURE, 1);
              writeEvent(fd, EV_KEY, BTN_TOUCH, 1);
              writeEvent(fd, EV_SYN, SYN_REPORT, 0);
       }
       return RET_OK;
}

我再以同样的方式实现了移动手指和松开手指的函数后,就只需要将这些函数封装成各种操作的函数就可以供调用了。如下面代码所以之的单击和拖动的函数, 也可以根据自己的需要实现长按,双击等操作。

int TouchUtil::touch(const int x, const int y, const int interval)
{
       if(!isDeviceReady)
       {
              return RET_ERROR;
       }
       g_trackingID ++;
       down(x, y, 0, g_trackingID);
       usleep(interval);
       up(x, y, 0);
}
int TouchUtil::pan(const int fromX, const int fromY, const int toX, const int toY)
{
       if(!isDeviceReady)
       {
              return RET_ERROR;
       }
       char msg[128];
       memset(msg, 0, sizeof(msg));
       sprintf(msg, "pan(%d, %d, %d, %d)", fromX, fromY, toX, toY);
       LOGI(LOG_TAG, msg);
       g_trackingID ++;
       down(fromX, fromY, 0, g_trackingID);
       usleep(5);
       int deltaX = (toX - fromX) / 10;
       int deltaY = (toY - fromY) / 10;
       int tempX = fromX;
       int tempY = fromY;
       for(int i=0; i<9; i++)
       {
              tempX += deltaX;
              tempY += deltaY;
              pressAndMove(tempX, tempY, 0, g_trackingID);
              usleep(5);
       }
       up(toX, toY, 0);
       return      RET_OK;
}

为了在其他的测试程序中也能使用这种方法,我将以上实现的代码放到了一个jni工程中,编译成了一个.so文件。

四、 研究结果及需要继续解决的问题

笔者按以上的方法实现的用户touch模拟操作,在三星GT-i9300、MOTO Droid、小米等手机上进行了测试,并以SOSO地图作为实验对象,确认是可以较好的模拟用户操作。

在以上的研究中也还有一些待解决的问题,主要包括:

1. 不是所有的手机设备都完全遵循multi-touch的协议。例如multi-touch协议要求touch结束必须以SYN_MT_REPORT以及SYN_REPORT结束,大部分的手机都遵循该协议,可是示例中的三星GT-i9300手机结束的时候并没有发送SYN_MT_REPORT。目前我暂时的解决对这类手机放到一个适配表里,需要继续研究更好的方法。

2. 由于研究的时间较短,目前只是对少量的手机做到了zoom in和zoom out的模拟,还没有完全研究出针对大部分手机都适用的双指以及多指操作的模拟方法,这也是需要继续研究解决的问题。

以上就是我的一点心得,有什么不对或者可以改进的地方请大家不吝赐教,共同进步。

五、 参考文献

[1] Android_Input分析. http://wenku.baidu.com/view/1f6650906bec0975f465e22f.html

[2] S3C2440上touch screen触摸屏驱动.http://www.linuxidc.com/Linux/2011-10/45460.htm

[3] Android View动画与按键处理的分析. http://km.oa.com/group/533/articles/show/126065?kmref=search

[4] Android Touch Devices. http://source.android.com/tech/input/touch-devices.html

[5] Linux的Multi-touch协议. https://www.kernel.org/doc/Documentation/input/multi-touch-protocol.txt