Appium 自动化框架在 android 端有两种模式,Seledroid 和 Uiautomator。Seledroid 本身就提供了 Toast 信息检查的接口,所以无需考虑。但 Uiautomator 模式下,Toast 信息无法获取,google 官方也只是列为一个需求在后期实现,暂时没有提供合适的解决方案。
之前,我们运用了文字识别的方式来进行检查,但可以预见,稳定性和准确性都不是很高,而且文字识别工具依赖的一个应用在 Android5.0 之后的系统无法安装,重新适配也很麻烦。因此,干脆另寻出路。
安卓系统提供了一个辅助功能服务 AccessibilityService,通过它可以监听设备与用户之间的交互事件,包括各类界面操作、信息变化、通知等,而 Toast 即是通知事件中的一种。
因此,可以开发一个辅助应用,在脚本运行之前开启服务,自动记录运行过程中出现的 Toast 信息,然后返回到脚本进行检查。
1、 新建一个服务类,继承 AccessibilityService,重写 onAccessibilityEvent 方法:
public class ToastRecorder extends AccessibilityService {
@Override
public void onAccessibilityEvent(AccessibilityEvent event) {
// TODO Auto-generated method stub
// System.out.println("Enter->onAccessibilityEvent");
//判断是否是通知事件
if(event.getEventType() != AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED)
return;
//获取消息来源
String sourcePackageName = (String)event.getPackageName();
//获取事件具体信息
Parcelable parcelable = event.getParcelableData();
//如果是下拉通知栏消息
if(parcelable instanceof Notification){
} else {
//其它通知信息,包括Toast
String toastMsg = (String) event.getText().get(0);
String log = "Latest Toast Message: "+toastMsg+" [Source: "+sourcePackageName+"]";
System.out.println(log);
}
}
@Override
public void onInterrupt() {
// TODO Auto-generated method stub
}
}
2、 新建一个入口 activity,启动上述服务
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent toastIntent = new Intent(MainActivity.this, ToastRecorder.class);
startService(toastIntent);
}
}
3、 在 AndroidManifest 文件中注册服务,添加权限并通过 meta-data 配置该服务
<service android:name="com.xbin.toastchecker.ToastRecorder"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
android:enabled="true">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice" />
</service>
Accessibilityservice 文件内容:
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeNotificationStateChanged"
android:accessibilityFeedbackType="feedbackAllMask"
android:notificationTimeout="100" />
以上,该辅助应用的基本功能就完成了,安装到终端上后,还需到 “设置——辅助功能——服务” 下开启对应的服务。
然后,就可以检查系统的 Toast 信息了:
查看应用打印的日志:
说明已经能捕获成功,那么下一步就是如何使用获取到得 Toast 信息,有两种比较简单的方法:
1) 在该辅助应用中再建一个 socket 服务,将每次获取到的 Toast 信息赋值给一个全局变量,自动化脚本中通过发送 socket 指令获取最新的 Toast 信息值进行检查。
2) 脚本直接通过 adb logcat 读取日志缓存,匹配获取最新一次 Toast 信息进行检查。
该应用进程被杀后,开启的服务会自动关闭,下次启动应用时要重新在辅助功能中启动该服务,暂时也没有找到能通过代码启动的办法,除非有系统级权限直接去写配置。
在 MainActivity 中添加以下代码:
Intent intent = new Intent(android.provider.Settings.ACTION_ACCESSIBILITY_SETTINGS);
startActivityForResult(intent, 0);
它会在应用启动的时候,直接调出辅助功能设置界面,然后就通过 GUI 自动化的办法点两下就好了。
很抱歉,发帖之前没有仔细验证,今天放在脚本中用的时候,发现有个致命问题:
Appium 启用 Uiautomator 建立 session 之后,所有的 AccessibilityService 服务全都挂死了,看上去是 Uiautomator 非常霸道的直接独占了 AccessibilityService,毕竟底层就是用它来执行操作的,当然,也可能是 Uiautomator 的一个 Bug。但不管怎样,这种方式貌似是行不通了,让大家见笑,权当是给大家提供一种思路吧。
不过,为此,我今天又试了另一种黑科技——Xposed 框架,这个就更好玩了。
关于 Xposed 框架我就不多做说明了,网上一搜一大堆资料,ROM 玩家的必备神技,简单点说就是通过 Hook 系统方法来注入用户行为。
所以,我们要做的就是 Hook Toast 对象的 show 方法,在它执行之前将其中的文本取出打印即可。
代码也不多,如下:
public class XposedHook implements IXposedHookZygoteInit {
@Override
public void initZygote(StartupParam startupParam) throws Throwable {
//设定hook目标类和方法
XposedHelpers.findAndHookMethod(Toast.class, "show", new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
//获取Toast对象
Toast t = (Toast) param.thisObject;
try {
//获取唯一的TextView,即Toast文本
View view = t.getView();
List<TextView> list = new ArrayList<TextView>();
if (view instanceof TextView) {
list.add((TextView) view);
} else if (view instanceof ViewGroup) {
finaAllTextView(list, (ViewGroup) view);
}
if (list.size() != 1) {
throw new RuntimeException("number of TextViews in toast is not 1");
}
TextView text = list.get(0);
//获取文本内容
CharSequence toastMsg = text.getText();
System.out.println("XposedHookToast:"+toastMsg);
} catch (RuntimeException e) {
XposedBridge.log(e);
}
}
});
}
//获取对象中的所有TextView
private void finaAllTextView(List<TextView> addTo, ViewGroup view) {
int count = view.getChildCount();
for (int i = 0; i < count; ++i) {
View child = view.getChildAt(i);
if (child instanceof TextView) {
addTo.add((TextView) child);
} else if (child instanceof ViewGroup) {
finaAllTextView(addTo, view);
}
}
}
}
安装 Xposed 框架、激活再安装该模块后,也能起到获取 Toast 文本信息的作用。
Line 5251: I/System.out( 815): XposedHookToast:登录失败,可能原因是用户名或密码错误、密码过期或者帐号锁定
Line 5959: I/System.out( 815): XposedHookToast:连接服务器失败
但由于需要 root,而且也不是所有终端都能顺利安装 Xposed 框架,这也不算是一种可通用的方法。
容我再想想。