Fastbot_Android icon indicating copy to clipboard operation
Fastbot_Android copied to clipboard

判断坐标是否位于黑控件区域逻辑问题

Open BirdLearn opened this issue 1 year ago • 13 comments

黑控件仍然被点击到问题

复现场景

使用 fastbot 进行自动化测试时,偶现点击到黑控件区域,导致不符合预期的行为。

  • 使用 max.widget.black 配置如下
[{
	"activity": "com.xegale.mobile.main.MainActivity",
	"xpath": "//*[@resource-id='com.xeagle.mobile:id/sniff_default_iv']"
}, {
	"activity": "com.xegale.mobile.main.MainActivity",
	"xpath": "//*[@resource-id='com.xeagle.mobile:id/bottom_tab_layout']"
}]

在实际使用中发现,即使配置了黑控件区域,仍然偶现点击到黑控件区域。

问题分析

使用 uiautomatorviewer 工具,识别黑控件区域坐标为 [0,2160][1080,2259],即在该区域下的坐标应被判定为不可点击区域。 通过走查日志发现:Fastbot 有点击 (682.29974,2224.0) 的操作,该坐标位于黑控件区域内部。

  • 分析 java 层日志
[Fastbot][2024-03-20 16:47:58.428] :Sending Touch (ACTION_DOWN): 0:(682.29974,2224.0)
[Fastbot][2024-03-20 16:47:58.436] Wait Event for 1000 milliseconds
[Fastbot][2024-03-20 16:47:59.443] :Sending Touch (ACTION_UP): 0:(682.29974,2224.0)
  • 分析 native 层日志
03-20 16:47:58.381 I/[Fastbot](18011): action type: LONG_CLICK
03-20 16:47:58.382 I/[Fastbot](18011): rpc cost time: 173
03-20 16:47:58.382 I/[Fastbot](18011): check point [568, 982] is  in black widgets
03-20 16:47:58.382 I/[Fastbot](18011): check point [716, 487] is  in black widgets
03-20 16:47:58.383 I/[Fastbot](18011): check point [653, 172] is  in black widgets
03-20 16:47:58.383 I/[Fastbot](18011): check point [300, 1900] is  in black widgets
03-20 16:47:58.383 I/[Fastbot](18011): check point [1013, 415] is  in black widgets
03-20 16:47:58.384 I/[Fastbot](18011): check point [208, 748] is  in black widgets
03-20 16:47:58.384 I/[Fastbot](18011): check point [357, 451] is  in black widgets
03-20 16:47:58.384 I/[Fastbot](18011): check point [632, 1117] is  in black widgets
03-20 16:47:58.385 I/[Fastbot](18011): check point [230, 811] is  in black widgets
03-20 16:47:58.385 I/[Fastbot](18011): check point [456, 1360] is  in black widgets
03-20 16:47:58.385 I/[Fastbot](18011): check point [682, 2224] is  in black widgets
03-20 16:47:58.387 I/[Fastbot](18011):  event time:499
03-20 16:47:58.387 I/[Fastbot](18011): :Sending rotation degree=0, persist=false
03-20 16:47:57.696 I/WindowManager( 1624): navColorWin was set to default navbar color so should add SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 8518
03-20 16:47:58.388 I/[SPMN]  ( 1624): insert SYSTEM -- name = accelerometer_rotation, package = android, user = 0, value = 0
03-20 16:47:58.388 I/HwWindowManagerServiceEx( 1624): setLandAnimationInfo : false
03-20 16:47:58.390 I/WindowManager( 1624): navColorWin was set to default navbar color so should add SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 8518
03-20 16:47:58.393 I/HwWindowManagerServiceEx( 1624): setLandAnimationInfo : false
03-20 16:47:58.397 I/WindowManager( 1624): navColorWin was set to default navbar color so should add SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 8518
03-20 16:47:58.403 I/[SPMN]  ( 1624): insert SYSTEM -- name = accelerometer_rotation, package = android, user = 0, value = 1
03-20 16:47:58.405 I/WindowManager( 1624): navColorWin was set to default navbar color so should add SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 8518
03-20 16:47:58.407 I/HwWindowManagerServiceEx( 1624): setLandAnimationInfo : false
03-20 16:47:58.415 I/WindowManager( 1624): navColorWin was set to default navbar color so should add SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 8518
03-20 16:47:58.420 I/HwWindowManagerServiceEx( 1624): setLandAnimationInfo : false
03-20 16:47:58.423 I/WindowManager( 1624): navColorWin was set to default navbar color so should add SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 8518
03-20 16:47:58.429 I/[Fastbot](18011): :Sending Touch (ACTION_DOWN): 0:(682.29974,2224.0)
  • 分析 java 层代码
    private PointF shieldBlackRect(PointF p) {
        // move to native: AiClient.checkPointIsShield
        int retryTimes = 10;
        PointF p1 = p;
        do {
            if (!AiClient.checkPointIsShield(this.currentActivity, p1)) {
                break;
            }
            // re generate a point
            Rect displayBounds = AndroidDevice.getDisplayBounds();
            float unitx = displayBounds.height() / 20.0f;
            float unity = displayBounds.width() / 10.0f;
            p1.x = p.x + retryTimes * unitx * RandomHelper.nextInt(8);
            p1.y = p.y + retryTimes * unity * RandomHelper.nextInt(17);
            p1.x = p1.x % displayBounds.width();
            p1.y = p1.y % displayBounds.height();
        } while (retryTimes-- > 0);
        return p1;
    }
  • 测试出错原因

check point [456, 1360] is in black widgets 黑控件区域判断出错,导致一直识别为黑控件区域,耗尽重试次数后,偶现点击到黑控件区域

  • native 层判断是否命中黑控件区域逻辑,该函数代码位于 /native/events/Preference.cpp 文件 335 行
    bool Preference::checkPointIsInBlackRects(const std::string &activity, int pointX, int pointY) {
        bool isInsideBlackList;
        auto iter = this->_cachedBlackWidgetRects.find(activity);
        isInsideBlackList = iter != this->_cachedBlackWidgetRects.end();
        if (isInsideBlackList) {
            const Point p(pointX, pointY);
            for (const auto &rect: iter->second) {
                if (rect->contains(p)) {
                    isInsideBlackList = true;
                    break;
                }
            }
        }
        BLOG("check point [%d, %d] is %s in black widgets", pointX, pointY,
             isInsideBlackList ? "" : "not");
        return isInsideBlackList;
    }
  • 逻辑分析 这段代码的目的是检查给定的点 (pointX, pointY) 是否位于与活动(activity)相关联的黑色矩形列表中的某个矩形内部。代码存在一个逻辑错误: 在检查点是否在黑色矩形内部时,如果 iter 存在(即 activity 对应黑控件存在),则将 isInsideBlackList 设置为 true,这是不对的,因为这只能做为是否应该在黑控件区域内部的判断条件,而不是结果。在遍历黑色矩形列表时,如果找到一个包含点的矩形,则将 isInsideBlackList 再次设置为 true,但是由于 isInsideBlackList 为true ,当遍历完所有的区域,isInsideBlackList 仍然为 true,此时实际结果应该是 false。即当前的坐标不存在于当前activity中的任何黑控件中。 实际上,当前版本的代码,在这种情况下,仍然做了true的判定,会导致即使遍历完所有的屏幕分区,仍然无法找到一个不存在于黑控件区域的坐标,然后耗尽重试次数,导致最终点击的坐标不可控。

解决方案

为了修复这个问题,我们可以在找到一个包含点的矩形后立即返回 true,而不是继续遍历其他矩形。如果没有找到包含点的矩形,则最终返回 false。

  • 正确逻辑应该如下
bool Preference::checkPointIsInBlackRects(const std::string &activity, int pointX, int pointY) {
    bool isInsideBlackList = false;
    auto iter = this->_cachedBlackWidgetRects.find(activity);
    if (iter != this->_cachedBlackWidgetRects.end()) {
        const Point p(pointX, pointY);
        for (const auto &rect : iter->second) {
            if (rect->contains(p)) {
                isInsideBlackList = true;
                break; // Found, no need to continue checking
            }
        }
    }
    BLOG("check point [%d, %d] is %s in black widgets", pointX, pointY,
         isInsideBlackList ? "" : "not");
    return isInsideBlackList;
}

BirdLearn avatar Mar 20 '24 10:03 BirdLearn

修改上述提到的问题后,仍然有发现黑控件不生效的情况,通过走查日志发现另一个可能的代码逻辑问题。

在日志中看到配置中的多个 black widget,在 native 层日志均有打印识别到,但是在运行到实际判断 point 是否位于黑控件区域时,却打印 Rects: 1 ,即当前Activity中仅保存有一个黑控件区域,这与配置中的多个黑控件不符,因此导致黑控件不生效。

  • 通过走查代码发现,在处理黑控件时逻辑如下
void Preference::resolveBlackWidgets(const ElementPtr &rootXML, const std::string &activity) {
        // black widgets
        if (!this->_blackWidgetActions.empty()) {
            for (const CustomActionPtr &blackWidgetAction: this->_blackWidgetActions) {
                if (!activity.empty() && blackWidgetAction->activity != activity)
                    continue;
                XpathPtr xpath = blackWidgetAction->xpath;
                // read the bounds of black widget from the config
                std::vector<float> bounds = blackWidgetAction->bounds;
                bool hasBoundingBox = bounds.size() >= 4;
                if (nullptr == this->_rootScreenSize) {
                    BLOGE("black widget match failed %s", "No root node in current page");
                    return;
                }
                if (hasBoundingBox && bounds[1] <= 1.1 && bounds[3] <= 1.1) {
                    int rootWidth = this->_rootScreenSize->right;// - rootSize->left;
                    int rootHeight = this->_rootScreenSize->bottom;// - rootSize->top;
                    bounds[0] = bounds[0] * static_cast<float>(rootWidth);
                    bounds[1] = bounds[1] * static_cast<float>(rootHeight);
                    bounds[2] = bounds[2] * static_cast<float>(rootWidth);
                    bounds[3] = bounds[3] * static_cast<float>(rootHeight);
                }
                bool xpathExistsInPage;
                std::vector<ElementPtr> xpathElements;
                if (xpath) {
                    this->findMatchedElements(xpathElements, xpath, rootXML);
                    BDLOG("find black widget %s  %d", xpath->toString().c_str(),
                          (int) xpathElements.size());
                }
                xpathExistsInPage = xpath && !xpathElements.empty();
                std::vector<RectPtr> cachedRects;  // cache black widgets

                if (xpathExistsInPage && !hasBoundingBox) {
                    BLOG("black widget xpath %s, has no bounds matched %d nodes",
                         xpath->toString().c_str(), (int) xpathElements.size());
                    for (const auto &matchedElement: xpathElements) {
                        BLOG("black widget, delete node: %s depends xpath",
                             matchedElement->getResourceID().c_str());
                        cachedRects.push_back(matchedElement->getBounds());
                        matchedElement->deleteElement();
                    }
                }
                else if (xpathExistsInPage || (!xpath && hasBoundingBox)) {
                    RectPtr rejectRect = std::make_shared<Rect>(bounds[0], bounds[1], bounds[2],
                                                                bounds[3]);
                    cachedRects.push_back(rejectRect);
                    std::vector<ElementPtr> elementsInRejectRect;
                    rootXML->recursiveElements([&rejectRect](const ElementPtr &child) -> bool {
                        return rejectRect->contains(child->getBounds()->center());
                    }, elementsInRejectRect);
                    BLOG("black widget xpath %s, with bounds matched %d nodes",
                         xpath ? xpath->toString().c_str() : "none",
                         (int) elementsInRejectRect.size());
                    for (const auto &elementInRejectRect: elementsInRejectRect) {
                        if (elementInRejectRect) {
                            BLOG("black widget, delete node: %s depends xpath",
                                 elementInRejectRect->getResourceID().c_str());
                            elementInRejectRect->deleteElement();
                        }
                    }
                }
                this->_cachedBlackWidgetRects[activity] = cachedRects;
            }
        }
    }

以上代码中,在 for 循环内定义了一个变量 std::vector<float> bounds = blackWidgetAction->bounds;,并在当前for 循环结束时,将识别到的cachedRects 赋值给 _cachedBlackWidgetRects Map 中的 activity 对应的值,这样导致在循环结束后,只有最后一个黑控件的区域被保存在 _cachedBlackWidgetRects 中,因此导致黑控件不生效。

解决方案

修改 resolveBlackWidgets 函数,将 cachedRects 定义在 for 在 for 循环外部,这样在循环结束后,所有的黑控件区域都会被保存在 _cachedBlackWidgetRects 中,代码如下

    void Preference::resolveBlackWidgets(const ElementPtr &rootXML, const std::string &activity) {
        // black widgets
        if (!this->_blackWidgetActions.empty()) {
            std::vector<RectPtr> cachedRects;  // cache black widgets
            for (const CustomActionPtr &blackWidgetAction: this->_blackWidgetActions) {
                // ... ...
            }
            this->_cachedBlackWidgetRects[activity] = cachedRects;
        }
    }

能力有限,无法肯定该问题定位是否准确,并且修改有效,劳烦项目管理员百忙之后可以抽时间审查一下,万分感谢

BirdLearn avatar Mar 22 '24 03:03 BirdLearn

你好,后面这部分修改 是否已存在于你第一个问题提供的 so包链接中?

liuts933 avatar Jul 18 '24 03:07 liuts933

@BirdLearn 忍不住给大佬点赞。大佬有遇到过配置的自定义事件不生效的情况吗?

lilylei665 avatar Sep 18 '24 03:09 lilylei665

@BirdLearn 忍不住给大佬点赞。大佬有遇到过配置的自定义事件不生效的情况吗?

自定义事件如果是原生安卓应该还好,我这边配了很多,基本使用都正常。 这块比较考验你们对前端UI的分析,我们配的时候也不是一次就搞定,得反复调试几次, 一般都是 activity 不对或者 元素选错,得确认要操作的元素是否支持对应的能力,比如click,write。

liuts933 avatar Sep 18 '24 09:09 liuts933

你好,后面这部分修改 是否已存在于你第一个问题提供的 so包链接中?

fork了一份代码,并且把修改的代码和库文件都更新上去了,可以看一下这个仓库中的lib库文件,https://github.com/BirdLearn/Fastbot_Android/tree/main/libs

BirdLearn avatar Sep 18 '24 10:09 BirdLearn

@BirdLearn 忍不住给大佬点赞。大佬有遇到过配置的自定义事件不生效的情况吗?

这个Fastbot 内部有很多隐含的逻辑在里面,并且文档中并没有详细说明,在调试的时候,需要可能需要结合日志与代码一起来分析为何action不生效,上面的同学回复的经验可以参考

另外:这里说一个隐含的规则,就是如何你的action中的控件是一个可输入的控件,例如输入框之类的话,默认fastbot 会根据规则随机或者在定义的字符串列表中去做输入,如果你需要在输入框控件上进行 keyevent 输入的话,就没法实现,我最新的代码和库文件中对这个逻辑进行了修改,新增 editable 字段进行默认输入控制,如果 editable 设置为 false的话,就不会有随机输入行为

以下是个例子

[{
		"prob": 0.2,
		"activity": "com.xxxxx.mobile.main.MainActivity",
		"times": 1000000,
		"actions": [{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/input_edit']",
					"action": "CLICK",
					"index": 0,
					"throttle": 1000,
					"editable": false
				},
				{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/search_really_edit']",
					"action": "CLICK",
					"index": 0,
					"throttle": 3000,
					"editable": true,
					"text": "",
					"clearText": true,
					"useAdbInput": true
				},
				{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/search_really_edit']",
					"action": "SHELL_EVENT",
					"command": "input keyevent 66",
					"editable": false
				}]
	}
]

BirdLearn avatar Sep 18 '24 10:09 BirdLearn

@BirdLearn 忍不住给大佬点赞。大佬有遇到过配置的自定义事件不生效的情况吗?

这个Fastbot 内部有很多隐含的逻辑在里面,并且文档中并没有详细说明,在调试的时候,需要可能需要结合日志与代码一起来分析为何action不生效,上面的同学回复的经验可以参考

另外:这里说一个隐含的规则,就是如何你的action中的控件是一个可输入的控件,例如输入框之类的话,默认fastbot 会根据规则随机或者在定义的字符串列表中去做输入,如果你需要在输入框控件上进行 keyevent 输入的话,就没法实现,我最新的代码和库文件中对这个逻辑进行了修改,新增 editable 字段进行默认输入控制,如果 editable 设置为 false的话,就不会有随机输入行为

以下是个例子

[{
		"prob": 0.2,
		"activity": "com.xxxxx.mobile.main.MainActivity",
		"times": 1000000,
		"actions": [{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/input_edit']",
					"action": "CLICK",
					"index": 0,
					"throttle": 1000,
					"editable": false
				},
				{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/search_really_edit']",
					"action": "CLICK",
					"index": 0,
					"throttle": 3000,
					"editable": true,
					"text": "",
					"clearText": true,
					"useAdbInput": true
				},
				{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/search_really_edit']",
					"action": "SHELL_EVENT",
					"command": "input keyevent 66",
					"editable": false
				}]
	}
]

输入控件,我这边业务有那种text的,password的。 还有那种密码不是一个空栏,是6个小方块的。都可以正常输入,需要安装并配置好 ADBkeyboard 。 从我使用的经验分析,他原本的逻辑好像是切换activity后 先判断自定义事件命中没有,如果命中了就会按顺序执行完自定义事件。没命中就会执行随机事件。 因为APP测试过程中会有意外登出,这时需要登录回来;金融APP还可能会触发APP锁屏页需要解锁。反正如果配置自定义事件没有弄错,都是生效的,不需要修改原有逻辑,你这么改感觉有风险,而且不好懂,"editable": false的情况反而可以输入,有点反人类。 另外有个特点,貌似没有id的元素好像它找不到,他的XPATH不能按照传统的xpath去理解。

大佬有没有QQ,加一下,交流点信息。关于覆盖率的。

liuts933 avatar Sep 18 '24 10:09 liuts933

@BirdLearn 忍不住给大佬点赞。大佬有遇到过配置的自定义事件不生效的情况吗?

这个Fastbot 内部有很多隐含的逻辑在里面,并且文档中并没有详细说明,在调试的时候,需要可能需要结合日志与代码一起来分析为何action不生效,上面的同学回复的经验可以参考 另外:这里说一个隐含的规则,就是如何你的action中的控件是一个可输入的控件,例如输入框之类的话,默认fastbot 会根据规则随机或者在定义的字符串列表中去做输入,如果你需要在输入框控件上进行 keyevent 输入的话,就没法实现,我最新的代码和库文件中对这个逻辑进行了修改,新增 editable 字段进行默认输入控制,如果 editable 设置为 false的话,就不会有随机输入行为 以下是个例子

[{
		"prob": 0.2,
		"activity": "com.xxxxx.mobile.main.MainActivity",
		"times": 1000000,
		"actions": [{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/input_edit']",
					"action": "CLICK",
					"index": 0,
					"throttle": 1000,
					"editable": false
				},
				{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/search_really_edit']",
					"action": "CLICK",
					"index": 0,
					"throttle": 3000,
					"editable": true,
					"text": "",
					"clearText": true,
					"useAdbInput": true
				},
				{
					"xpath": "//*[@resource-id='com.xxxxx.mobile:id/search_really_edit']",
					"action": "SHELL_EVENT",
					"command": "input keyevent 66",
					"editable": false
				}]
	}
]

输入控件,我这边业务有那种text的,password的。 还有那种密码不是一个空栏,是6个小方块的。都可以正常输入,需要安装并配置好 ADBkeyboard 。 从我使用的经验分析,他原本的逻辑好像是切换activity后 先判断自定义事件命中没有,如果命中了就会按顺序执行完自定义事件。没命中就会执行随机事件。 因为APP测试过程中会有意外登出,这时需要登录回来;金融APP还可能会触发APP锁屏页需要解锁。反正如果配置自定义事件没有弄错,都是生效的,不需要修改原有逻辑,你这么改感觉有风险,而且不好懂,"editable": false的情况反而可以输入,有点反人类。 另外有个特点,貌似没有id的元素好像它找不到,他的XPATH不能按照传统的xpath去理解。

大佬有没有QQ,加一下,交流点信息。关于覆盖率的。

这个XPATH不是所有的布局信息中的控件元素都能找到,内部做了一层过滤,只有是可点击和客输入的控件才能查找到,你在执行的时候可以指定 -v -v -v 查看详细的 debug 日志,会有过滤后的布局文件信息输出

BirdLearn avatar Sep 18 '24 10:09 BirdLearn