Android TV磁贴类app自动化框架二次改造(基于UiAutomator)

简介

项目一直是手工测试为主,加上一直是TV类应用,很多自动化工具都没有针对TV类项目做很好的适配,所以只有自己动手了。主要针对项目的特殊性进行了部分改造,不一定适用于其他项目。(涉及隐私,就不提供json文件和软件名字啦)

痛点

1.非标准控件的难处
Android TV磁贴类app自动化框架二次改造(基于UiAutomator)_第1张图片
通过uiautomatorviewer获取到的不一样的磁贴,属性全部相同(除了坐标点),意味着没法通过id和class+index方式获取,text属性为空,也就没有办法通过byText的方式获取uiobject,高度定制的磁贴,让自动化很为难,如果通过坐标点,太坑爹,不能跨设备,还是坑。

2.TV类应用没有触摸操作
TV类安卓程序,主要面向的是遥控器,也就是接收的是keyevent,所以touch事件显得不这么全面,为了最接近用户,还是选择用key来做自动化。

架构模块

Android TV磁贴类app自动化框架二次改造(基于UiAutomator)_第2张图片

解析模块

从服务器解析json文件格式,包装成实体类CellInfo,返回一个包含磁铁信息的List。一个磁铁对应一个CellInfo,一个CellInfo需要提取的信息有

  • x坐标
  • y坐标
  • 所属的Tab分类页
  • 每个磁铁的说明标签

所以对应的定义如下:

package launcherClick.model;

public class CellInfo {

    private String label;
    private String tab;

    public String getTab() {
        return tab;
    }

    public void setTab(String tab) {
        this.tab = tab;
    }

    private int x;
    private int y;

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

}

解析用的是第三方开源库org.json,对应的解析代码如下:

package launcherClick;

import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONObject;

import Utils.IOUtils;
import Utils.Println;
import launcherClick.model.CellInfo;

public class AMetroParse {

    public static List<CellInfo> startParse(String url) {
        String str = IOUtils.readFromNet(url);
        if(str==null){
            return null;
        }
        JSONObject jsonObject = new JSONObject(str);
        List<CellInfo> list = new ArrayList<CellInfo>();
        JSONArray _tabs = jsonObject.getJSONArray("tabs");
        for(int i = 0;i<_tabs.length();i++){
            String tab = _tabs.getJSONObject(i).getString("label");
            JSONArray _cells = _tabs.getJSONObject(i).getJSONArray("cells");
            for(int j=0;j<_cells.length();j++){
                CellInfo cellInfo = new CellInfo();
                cellInfo.setX(_cells.getJSONObject(j).getJSONObject("location").getInt("x"));
                cellInfo.setY(_cells.getJSONObject(j).getJSONObject("location").getInt("y"));
                cellInfo.setLabel(_cells.getJSONObject(j).getJSONObject("content").getString("label"));
                cellInfo.setTab(tab);
                list.add(cellInfo);
            }
        }
        return list;
    }
}

坐标转换模块

这部分的工作是把上一步骤解析后的实体类进行提取和处理,主要处理的内容是根据x y坐标计算磁铁移动量,更具x的极大值和分类页做跨分类移动的偏移计算以及一些其他处理。
通过x y计算偏移比较简单,只是简单计算距离:

        for (CellInfo cellInfo : list) {
            if (cellInfo.getLabel().contains(label)
                    && cellInfo.getTab().contains(tab)) {
                int x = cellInfo.getX();
                int y = cellInfo.getY();
                new Println("x:" + x + " " + "y:" + y + " " + "偏移量" + _offset);
                // x方向
                for (int i = x + _offset; i > 1; i--) {
                    new Println("→" + " count is " + (x + _offset));
                    keyRight();

                }
                // y方向
                for (int i = y; i > 1; i--) {
                    new Println("↓");
                    keyDown();
                }
            }
        }

恩,里面有个偏移量,跨分类用的,偏移量是通过每个分类下磁铁最大值得出来的,每一个分类最后一个磁铁的x值即为最大值,遍历当前分类下的所有磁铁的x,如果大于后面一个,则放在前面,下次再用这个值去比较下面的x值。忘记这是什么排序算法了…囧…当然,也可以用Collections的自带的算法。

    private int togicOffset(String tab) {
        int lineLenth = 0;
        List<CellInfo> list = AMetroParse
                .startParse("我是隐藏的接口");
        for (CellInfo cellInfo : list) {
            if (cellInfo.getTab().contains(tab)) {
                int x = cellInfo.getX();
                lineLenth = lineLenth >= x ? lineLenth : x;
            }
        }
        return lineLenth;
    }

那么通过上面的排序之后,就可以得到每个分类的偏移量,这样对于后面的分类磁贴,就可以知道磁贴在整体的真正位置啦,于是就可以开始移动了。

        for (CellInfo cellInfo : list) {
            if (cellInfo.getLabel().contains(label) && cellInfo.getTab().contains(tab)) {
                int x = cellInfo.getX();
                int y = cellInfo.getY();
                new Println("x:" + x + " " + "y:" + y + " " + "偏移量" + _offset);
                // x方向
                for (int i = x + _offset; i > 1; i--) {
                    new Println("→" + " count is " + (x + _offset));
                    keyRight();

                }
                // y方向
                for (int i = y; i > 1; i--) {
                    new Println("↓");
                    keyDown();
                }
            }
        }

图片比较模块

来源于monkeyrunner的思路,原理是计算两个bitmap的长宽,然后提取出里面每个像素的像素值,如果相同,相似度+1,最后再除以总像素值(比如1280x720),这样就可以把相似程度转为一个一个可以衡量的具体值了,那么判断这个界面是不是我需要点击的时候,只需要截屏当前图片和预期的图片对比,相似度达到100的时候就认为是正确的。当然也提供了局部比较的功能,比如获取不到数据的异常提示。那么代码如下,提供了多种重载的方法以及两个assert判断。

package Utils;

import junit.framework.Assert;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

public class ImageCompare {

    public  static void AssertBitmapEqual(Bitmap bitmap0, Bitmap bitmap1){
        /** * 百分百图片相同断言-.- */
        Assert.assertEquals(100, ImageCompare(bitmap0,bitmap1));
    }

    public  static void AssertBitmapNotEqual(Bitmap bitmap0, Bitmap bitmap1){
        /** * 百分之零图片相同断言 */
        Assert.assertEquals(0, ImageCompare(bitmap0,bitmap1));
    }

    public static int ImageCompare(String path0,String path1) {
        /** * 提供根据路径直接比较 */
        Bitmap bitmap0 = BitmapFactory.decodeFile(path0); 
        Bitmap bitmap1 = BitmapFactory.decodeFile(path1); 
        return ImageCompare(bitmap0,bitmap1);
    }

    public static int ImageCompareChild(Bitmap bitmap0, Bitmap bitmap1,int x,int y,int width,int height) {
        /** * 裁剪子图并比较,主要是为了解决拉取动态数据不同,但是局部提示不变的比较场景。 */
        Bitmap bitmap00 = bitmap0.createBitmap(bitmap0, x, y, width, height);
        Bitmap bitmap01 = bitmap1.createBitmap(bitmap1, x, y, width, height);
        return ImageCompare(bitmap00,bitmap01);
    }

    public static int ImageCompare(Bitmap bitmap0, Bitmap bitmap1) {
        /** * 比较的主函数 * 只能比较相同长宽的图片,不相等返回-1失败 * 相似度为1~100 * 原理是提取每一个像素点比较,整张图相似度取决于像素点相同个数,所以还是比较准确的 */
        int picPct = 0;
        int picCount = 0;
        int picCountAll = 0;
        new Println("begin to compare");
        if (bitmap0 == null || bitmap1 == null) {
            new Println("null bitmap");
            return -1;
        }
        if (bitmap0.getWidth() != bitmap1.getWidth()
                || bitmap0.getHeight() != bitmap1.getHeight()) {
            return -1;
        }
        new Println("宽度为:" + bitmap1.getWidth() + "高度为:" + bitmap1.getHeight());
        for (int j = 0; j < bitmap1.getWidth(); j++) {
            for (int i = 0; i < bitmap0.getHeight(); i++) {
                if (bitmap0.getPixel(j, i) == bitmap1.getPixel(j, i)) {
                    picCount++;
                }
                picCountAll++;
            }
        }
        int result = (int) (((float) picCount) / picCountAll * 100);
        new Println(picCount + "/" + picCountAll);
        new Println("相似度为:" + result);
        return result;
    }
}

异常&&其他模块

定义了一些异常类,主要功能用于提示,这个提示的作用后面会用到。
增加了启动应用和关闭应用的方法,原理是用到了shell命令。

Runtime.getRuntime().exec("am start -n 我是包名隐藏者");

初始化方法,主要用于异常时能够一键重新开始,以及磁贴复原功能,还有其他一些小的处理就不多说啦,下面开始持续构建。

自动化的持续集成

还是那句话,不持续集成的自动化不是自动化,所以这里介绍的是基于jenkins的自动化集成,其实UiAutomator做集成还是很容易的,只要把jar包放在一个固定的目录,然后shell命令执行就完事了。
Android TV磁贴类app自动化框架二次改造(基于UiAutomator)_第3张图片
然后定时任务自己选吧,构建失败的邮件提醒这里设置的是,上面我自定义的异常类,当控制台输出我的异常类,那么会认为不通过,然后触发邮件提醒。
Android TV磁贴类app自动化框架二次改造(基于UiAutomator)_第4张图片
那么至此,一个较为完整的流程就完成啦,那么在此框架上组员们(就我一个)就可以更进一部去完善二级页面的自动化用例了。

你可能感兴趣的:(android,自动化)