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

转载地址:https://testerhome.com/topics/3750


问题来源

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

容我再想想。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值