个人博客:
http://www.milovetingting.cn

基于Android的模拟点击探索

前言

压力测试中,一般会用到自动化测试。准备写一个APP,可以记录屏幕上的点击事件,然后通过shell命令来模拟自动执行。shell指令,比较容易实现。那么,关键的一步是获取点击的坐标。对于Android来说,为便于开发者调试,Android系统中的”开发者选项”中,有一个”指针位置”的选项。打开这个选项,点击屏幕,就会显示当前点击的位置坐标。接下来,来看一下打开选项的过程。

开发者选项页面

“开发者选项”的源码位于packages/apps/settings/src/com/android/settings/DevelopmentSettings.java文件中。

private SwitchPreference mPointerLocation;

在onCreate()方法中初始化:

mPointerLocation = findAndInitSwitchPref(POINTER_LOCATION_KEY);

findAndInitSwitchPref()方法:

1
2
3
4
5
6
7
8
9
private SwitchPreference findAndInitSwitchPref(String key) {
SwitchPreference pref = (SwitchPreference) findPreference(key);
if (pref == null) {
throw new IllegalArgumentException("Cannot find preference with key = " + key);
}
mAllPrefs.add(pref);
mResetSwitchPrefs.add(pref);
return pref;
}

当点击选项开关切换后,会把当前的开关状态存入Settings数据库。

1
2
3
4
private void writePointerLocationOptions() {
Settings.System.putInt(getActivity().getContentResolver(),
Settings.System.POINTER_LOCATION, mPointerLocation.isChecked() ? 1 : 0);
}

PhoneWindowManager

PhoneWindowManager的源码位于framework/base/services/core/java/com/android/server/policy/PhoneWindowManager.java文件中。

PhoneWindowManager会监听Settings.System.POINTER_LOCATION字段的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class SettingsObserver extends ContentObserver {
SettingsObserver(Handler handler) {
super(handler);
}

void observe() {
// Observe all users' changes
ContentResolver resolver = mContext.getContentResolver();
...
resolver.registerContentObserver(Settings.System.getUriFor(
Settings.System.POINTER_LOCATION), false, this,
UserHandle.USER_ALL);
...
updateSettings();
}

@Override public void onChange(boolean selfChange) {
updateSettings();
updateRotation(false);
}
}

当这个值发生变化时,在updateSettings()方法中调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public void updateSettings() {
ContentResolver resolver = mContext.getContentResolver();
boolean updateRotation = false;
synchronized (mLock) {
...

if (mSystemReady) {
int pointerLocation = Settings.System.getIntForUser(resolver,
Settings.System.POINTER_LOCATION, 0, UserHandle.USER_CURRENT);
if (mPointerLocationMode != pointerLocation) {
mPointerLocationMode = pointerLocation;
mHandler.sendEmptyMessage(pointerLocation != 0 ?
MSG_ENABLE_POINTER_LOCATION : MSG_DISABLE_POINTER_LOCATION);
}
}
...
}
synchronized (mWindowManagerFuncs.getWindowManagerLock()) {
PolicyControl.reloadFromSetting(mContext);
}
if (updateRotation) {
updateRotation(true);
}
}

在这个方法中,会通过Handler能送一个Message去处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private class PolicyHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ENABLE_POINTER_LOCATION:
enablePointerLocation();
break;
case MSG_DISABLE_POINTER_LOCATION:
disablePointerLocation();
break;
...
}
}
}

如果打开了”指针位置”的选项开关,那么会调用enablePointerLocation()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void enablePointerLocation() {
if (mPointerLocationView == null) {
mPointerLocationView = new PointerLocationView(mContext);
mPointerLocationView.setPrintCoords(false);
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.MATCH_PARENT);
lp.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
lp.flags = WindowManager.LayoutParams.FLAG_FULLSCREEN
| WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
if (ActivityManager.isHighEndGfx()) {
lp.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
lp.privateFlags |=
WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_HARDWARE_ACCELERATED;
}
lp.format = PixelFormat.TRANSLUCENT;
lp.setTitle("PointerLocation");
WindowManager wm = (WindowManager) mContext.getSystemService(WINDOW_SERVICE);
lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
wm.addView(mPointerLocationView, lp);
mWindowManagerFuncs.registerPointerEventListener(mPointerLocationView);
}
}

在这个方法中,首先初始化一个PointerLocationView对象,然后设置WindowManager.LayoutParams,然后将PointerLocationView实例添加到window中。再通过WindowManagerFuncs注册监听。

当屏幕上有点击时,会回调PointerLocationView的onPointerEvent()方法:

1
2
3
4
@Override
public void onPointerEvent(MotionEvent event) {
...
}

通过反射可以获取到PointerLocationView的实例,但是无法获取到WindowManagerFuncs实例。WindowManagerFuncs是在PhoneWindowManager的init()方法中初始化的。

1
2
3
4
5
6
7
8
@Override
public void init(Context context, IWindowManager windowManager,
WindowManagerFuncs windowManagerFuncs) {
mContext = context;
mWindowManager = windowManager;
mWindowManagerFuncs = windowManagerFuncs;
...
}

对于WindowManager的流程不了解。这种方法看来是行不通了。。。

在网上查了相关的资料,还有种方法是通过adb的getevent命令来获取/dev/input/路径下的event事件数据,然后解析相关数据。不过对于这块也不熟悉,就没有再深入研究。

总的来说,开发基于Android的模拟点击的应用是以失败告终。后面有时间再研究下是否有其它方法可以实现。