技术方案选择
Android测试支持库有:
- Junit3, Junit4:用于方法级别的单元测试,不通过手机运行,在测试一些正则表达式时,非常方便
- AndroidJUnitRunner:在手机上运行Junit测试,如一些需要获取Context的方法
- Espresso:UI 测试框架;适合应用中的功能性 UI 测试。
- UI Automator:UI 测试框架;适合跨系统和已安装应用的跨应用功能性 UI 测试
- 无障碍Api:可用于模拟用户点击,适合跨系统和已安装应用的跨应用功能性UI测试
选择结果:无障碍Api,因为UI Automator只能通过adb shell运行。
注意:Root后的手机,应该可以在App内直接执行UI Automator --- 没有经过测试
实现步骤
定时机制
定时机制很容易,使用AlarmManager就行,如下所示:
//点击,设置重复闹钟。
private void setRepeatingAlarm(){
Intent intent = new Intent(this, ClearWeixinActivity.class);
intent.putExtra("msg", "重复的事情多次提醒!!!");
intent.putExtra("type", "repeat");
PendingIntent pendingIntent = PendingIntent.getActivity(this, 101, intent, 0);
//假设当前时间15s之后,就开始第一次触发;然后每隔20s再次触发。
Calendar c = Calendar.getInstance();
c.set(Calendar.SECOND, c.get(Calendar.SECOND) + 60*60*1);
AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE);
alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
c.getTimeInMillis(),
1*60*60*1000, // 1个小时
pendingIntent);
}
注意: 当进程被杀后,闹钟无法调起应用,需要开启自启动服务
自动开启无障碍模式
由于无障碍模式的开启后,当应用程序进程被杀后,无障碍模式会被关掉,所以需要自动打开无障碍模式。
通过命令打开障碍模式的命令如下:
// 打开无障碍模式
adb shell settings put secure enabled_accessibility_services com.ly.robottool/com.ly.robottool.weixin.ClearWeixinService
adb shell settings put secure accessibility_enabled 1
// 查看无障碍的配置情况
adb shell content query --uri content://settings/secure
App里,通过获取Root权限后,可执行以上命令,如下所示:
try {
Process p = Runtime.getRuntime().exec("su");
DataOutputStream dos = new DataOutputStream(p.getOutputStream());
dos.writeBytes("settings put secure enabled_accessibility_services com.ly.robottool/com.ly.robottool.weixin.ClearWeixinService\n");
dos.writeBytes("settings put secure accessibility_enabled 1\n");
dos.writeBytes("mkdir /sdcard/333\n");
dos.writeBytes("exit\n");
dos.flush();
dos.close();
p.waitFor();
mHander.sendEmptyMessageDelayed(MESSAGE_ACCESSIBILITY_SUCCESS, 1000*1);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
注意: 上面的代码,需要在子线程里执行
无障碍服务
无障碍的整体机制
无障碍Api
更加详细的文档,请查看Android开发无障碍指南
总结一些关键点:
- 默认情况下,只能看到TextView及其ParentView,基他ImageView等等都看不到,但通过设置flags |= FLAG_INCLUDE_NOT_IMPORTANT_VIEWS后,可以看到其他没有包含TextView的View
- 只能看到标准View,即自定View的父类,无法看到自定View的类名
- 只能获取View的Parent,children,Text,ClassName,屏幕坐标,大小,viewId,一些状态(checkable,checked,focusable,focused,selected,clickable,longClickable)
注意:微信由于使用了资源id混淆技术,不同版本的微信apk,其viewid会变化
UIAutomatorViewer查看ID
uiautomatorviewer工具所在目录:Android SDK/tools/bin/uiautomatorviewer
与dumpsys比较:
结论:uiautomator,uiautomatorviewer,无障碍Api都只能看到TextView及其ParentView,但dumpsys可以看到全部View
微信自动清理聊天记录
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event == null) {
return;
}
if (!WECHAT_PACKAGENAME.equals(event.getPackageName())) {
return;
}
String beginUUID = SharedPreferenceUtils.getBeginUUID(this);
String endUUID = SharedPreferenceUtils.getEndUUID(this);
if(beginUUID == null) {
return ;
}
if(!beginUUID.equals(endUUID)) {
SharedPreferenceUtils.updateEndUUID(this, beginUUID);
hasClickMe = false;
hasClickSetting = false;
hasClickChat = false;
hasEnterClearDialog = false;
hasClickClear = false;
}
log("0000:" + event);
if(!hasClickMe) {
enterPerson(event);
return ;
}
if(!hasClickSetting){
enterSetting(event);
return ;
}
if(!hasClickChat){
enterChat(event);
return ;
}
if(!hasEnterClearDialog){
enterClearDialog(event);
return ;
}
if(!hasClickClear){
clickClear(event);
return ;
}
}
private void enterPerson(AccessibilityEvent event){
if(!"com.tencent.mm.ui.LauncherUI".equals(event.getClassName())){
return ;
}
if(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == event.getEventType()){
// 查找当前窗口中包含“安装”文字的按钮
List nodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId("com.tencent.mm:id/c3f");
AccessibilityNodeInfo myNode = null;
for(AccessibilityNodeInfo node : nodes){
if("我".equals(node.getText())) {
myNode = node;
}
}
if(myNode == null) {
return ;
}
myNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK);
hasClickMe = true;
}
}
参考
- Android开发无障碍指南