前言:appium/uiautomator 原生都没带有能获取 toast 的 api ,因此 UI 测试中获取 toast 暂时无解。但最近在论坛看到了这个帖子http://testerhome.com/topics/2346,里面提到其实可以获取到 Accessibility Service 其实是可以获取到 toast 的,所以今天探究一下到底是什么情况。

调试环境:

测试 apk:自制 apk,包含多个可以触发 toast 的按钮。
Android:Genymotion 模拟器:4.4.4, 4.1.1。真机:Flyme4.2.2.2C(base on Android 4.4.2)

验证是否能获取到 toast

1、按照帖子指引,下载并安装 Toaster应用(在 google play 上,请科学上网。)
(由于我需要验证不带有 google play 服务的模拟器,因此模拟器上用的是我从项目主页下载源码并用 gradle 自行编译的 apk 。)

编译过程:

$ git clone https://github.com/mars3142/toaster.git

修改toaster/app/build.gradle,在第 16 行插入:

lintOptions {
   abortOnError false
}

以忽略 lint 语法检查的错误。
然后运行gradle clean build就能编译出 app 了(由于只是 debug 用途,没有加签名)。apk 默认放在toast/app/build/apk/下,用 adb 安装 debug 版本的即可。
提示找不到 gradle 命令的同学,麻烦手动下载 gradle 后在 path 中加上 gradle/bin/的路径。

首次打开 Toaster 会提示你需要允许 Toaster 访问 Accessibility Service (需要跳转到系统设置菜单设置。无论原生 Rom 还是二次开发的 Rom,都必须由用户手动赋予权限)。允许后即可获取到所有(包括系统桌面、应用)toast 。

2、 打开被测 app ,手动通过点击按钮触发 toast

3、打开 Toaster 应用,查看记录到的 toast

兼容性:
真机(4.4.2):可用
模拟器(4.4.4):可用
模拟器(4.1.1):可用

探究关键代码

其实http://stackoverflow.com/questions/10659734/detecting-toast-messages已经说得比较清晰了。Toaster 中对应的实际代码:

src/main/java/org/mars3142/android/toaster/service/ToasterService.java

import android.accessibilityservice.AccessibilityService;
...
import android.view.accessibility.AccessibilityEvent;

...
public class ToasterService extends AccessibilityService {

private final static String TAG = ToasterService.class.getSimpleName();

@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
if (event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED) {
Log.d(TAG, "Unexpected event type");
return; // event is not a notification
}

// get notification infos
Calendar calendar = Calendar.getInstance();
long timestamp = calendar.getTimeInMillis();
String sourcePackageName = (String) event.getPackageName();
String message = "";
for (CharSequence text : event.getText()) {
message += text + "\n";
}
if (message.length() > 0) {
message = message.substring(0, message.length() - 1);
}

// record notification infos
Parcelable parcelable = event.getParcelableData();
if (!(parcelable instanceof Notification)) { // confirm it should be toast
ContentResolver cr = getContentResolver();
ContentValues cv = new ContentValues();
cv.put(ToasterTable.PACKAGE, sourcePackageName);
cv.put(ToasterTable.MESSAGE, message);
cv.put(ToasterTable.TIMESTAMP, timestamp);
cr.insert(ToasterTable.TOASTER_URI, cv);

Intent intent = new Intent("org.mars3142.android.toaster.APPWIDGET_UPDATE");
sendBroadcast(intent);
}
}

@Override
public void onInterrupt() {
// Nothing
}
}

主要做的事情就是继承`AccessibilityService`后重写了`onAccessibilityEvent`方法。在该方法内对`parceable`类型不是`Notification`(否则[它是手机顶部status bar的信息](http://tonysun3544.iteye.com/blog/1273055)。)且事件类型为`AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED`的消息进行记录(包括应用名、提示内容、出现时间、)。

`AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED` 官网相关文档:http://developer.android.com/reference/android/view/accessibility/AccessibilityEvent.html#TYPE_NOTIFICATION_STATE_CHANGED

# 集成到 appium 的可行性

由于需要使用AccessibilityService,所以应该不能集成到 bootstrap 中(bootstrap本质上是一个UIAutomator的测试用例,不能像普通应用那样调用系统服务)。但可以做成额外的 apk 集成到 appium 中(参考 [io.appium.settings](https://github.com/appium/io.appium.settings),一个用来实现wifi,数据连接及飞行模式切换的 app  ),然后通过 adb 命令进行控制。

后面我会参考 io.appium.settings 尝试制作一个可以记录和获取 toast 的简单 app 试试效果如何。不过如何获取返回值是个问题。。。









↙↙↙阅读原文可查看相关链接,并与作者交流