Appium 通过辅助工具进行安卓 Toast 文本检查的方法

虚冰丶夜 · 2015年12月09日 · 最后由 李宗旨 回复于 2018年09月05日 · 7488 次阅读
本帖已被设为精华帖!

问题来源

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 框架,这也不算是一种可通用的方法。

容我再想想。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 20 条回复 时间 点赞

挺巧妙的

不错。可以做为 appium 的帮助应用,在启动的时候,就安装被开启好。

不错。之前研究时找到的监控 toast 记录的应用原理和这个应该是一样的,只是那个功能多一些。
如果能写到 sd 卡的某个指定位置可能更好,更容易实现直接用 adb 命令获取 toast 信息,脚本写法更简单。

#1 楼 @doctorq @lihuazhang @chenhengjie123 @shixue33 感谢各位捧场,不过很抱歉,验证后发现不太可行。文章已更新,提供了另外一种方案,各位可以瞅瞅给点建议。我也尽量再想想有没有更通用的方法。

#5 楼 @xubin98246 你的方案我也试过,uiautomator 底层也是使用 accessibilityservice,每次运行 uiautomator 脚本或者使用 uiautomator dump,都会把 accessibilityservice 干掉。此外,请问如何在不把手机 root 的情况下 hook app 呢? 此外,请教下 hook 有哪几种方法呢?

或许可以和 ui2.0 结合使用

等楼主想想的结果,toast 现在用的还挺多的(还好一个页面只有一种 toast),自己没有 办法处理的,只能看下一个页面的控件进行判断了。

#6 楼 @heavennash 不 root hook 的方法可以看看这篇文章,但仅限应用自己的进程,非系统级 Hook。

把 xposed 单独再发文章吧. 这是个杀手级别的框架. 是测试利器. 只是很多人并不懂其中的奥妙

能给个 demo 么 我写了个竟然调不起 onAccessibilityEvent

#9 楼 @xubin98246 你好,Xposed hook 目标类和方法需要源码吗?

#12 楼 @xiaoluosun 有源码最好 不然就只能反编译了。总之需要明确的方法名和参数类型

#11 楼 @dongdong OK 了吗?之前没看到。基本需要的过程我文中应该都写了。

#14 楼 @xubin98246 弄好了 之前是因为 监控包没写对

UiAutomator 的 events 命令就可以抓取到 toast 上的文字,可以直接拿来用

#16 楼 @actionwind 请问你说的这个怎么用?

虚冰丶夜 [该话题已被删除] 中提及了此贴 08月26日 12:12
zuiniao123 [该话题已被删除] 中提及了此贴 09月25日 16:57
匿名 #21 · 2016年10月23日

今天搞了一整天,文章所说的都成功实行了,自己写的 APP 检查到 Toast 调用,去测要测的的 APP 竟然没有检测到,我要疯了,

#16 楼 @actionwind 在启用 Appium 的情况下,是无法执行这个命令的,马上就会退出 在没有 appium session 的情况下,打印出的日志确实包含 toast 的内容

思路很不错,谢谢,配合自己写的工具可以获取

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册