1
0
forked from BRT/arc
arc/android/rshb/ScreenXmlParser.cpp
trnsmkot ddd8095ca5 - Добавил xml дампы и скрины анализируемых экранов
- Добавил наработки по скрипту оплаты
2025-04-30 22:16:30 +07:00

326 lines
12 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ScreenXmlParser.h"
#include <QRegularExpression>
#include <QMap>
#include <QSet>
#include <QList>
#include <QXmlStreamReader>
#include <QDomDocument>
#include <QFile>
#include "android/xml/Node.h"
ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
}
ScreenXmlParser::~ScreenXmlParser() = default;
static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])");
Node ScreenXmlParser::tryToFindBannerOrDialog(const int width, const int height) {
// 1. Ищем push banner - он перекрывает весь экран
Node first = m_nodes[0];
for (const Node &node: m_nodes) {
// Если это однозначно, что пуш-баннер
if (node.resourceId == "aft-popup-pushNotification-iconClose" && node.className == "android.widget.Button") {
return node;
}
// Если есть кнопка "Позже"
if (node.text.trimmed() == "Позже" && node.className == "android.widget.Button") {
return node;
}
// Если нашли пустую кнпоку
if (node.className == "android.widget.Button" && node.clickable && node.text.isEmpty()) {
// <node class="android.widget.Button" package="ru.rshb.dbo" clickable="true" bounds="[630,142][691,205]"/>
if (node.x() > width / 2 && node.y() < width / 2) {
return node;
}
}
}
return {};
}
/**
* Смотрим что есть текс "Мы заботимся о вашей безопасности" и
* кнопка "Отложить"
* @return координаты кнопки для закрытия
*/
Node ScreenXmlParser::tryToFindSecurityBanner() {
Node text;
Node closeBtn;
for (const Node &node: m_nodes) {
if (node.text.trimmed() == "Мы заботимся о вашей безопасности" && node.className == "android.app.Dialog") {
text = node;
}
if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") {
closeBtn = node;
}
}
// Не нужно проверять все, если хотя бы одна координата есть
if (!text.isEmpty() && !closeBtn.isEmpty()) {
return closeBtn;
}
return {};
}
Node ScreenXmlParser::findTextNode(const QString &text) {
for (const Node &node: m_nodes) {
if (node.className == "android.widget.TextView" && node.text.trimmed() == text) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findTextNode(const QString &text, const int x1, const int y1, const int x2, const int y2) {
for (const Node &node: m_nodes) {
if (node.className == "android.widget.TextView" && node.text.trimmed() == text
&& node.x() >= x1 && node.x() <= x2 && node.y() >= y1 && node.y() <= y2) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findFirstEditText() {
for (const Node &node: m_nodes) {
if (node.className == "android.widget.EditText" && node.focusable) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findButtonNode(const QString &text, const bool contains) {
for (const Node &node: m_nodes) {
if (contains) {
if (node.className == "android.widget.Button" && node.text.contains(text)) {
return node;
}
} else {
if (node.className == "android.widget.Button" && node.text.trimmed() == text) {
return node;
}
}
}
return {};
}
bool ScreenXmlParser::isHomeScreen() {
// Есть заголовок "Счета и карты" и кнопка "Все продукты"
Node text;
Node allProductBtn;
for (const Node &node: m_nodes) {
if (node.text.trimmed() == "Счета и карты" && node.className == "android.widget.TextView") {
text = node;
}
if (node.text.trimmed() == "Все продукты" && node.className == "android.widget.Button") {
allProductBtn = node;
}
}
return !text.isEmpty() && !allProductBtn.isEmpty();
}
QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
QMap<QString, Node> buttons;
const QSet<QString> numberSet = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"};
for (const Node &node: m_nodes) {
const QString &text = node.text.trimmed();
if (node.className == "android.widget.Button" && numberSet.contains(text)) {
buttons[text] = node;
}
}
QList<Node> nodes;
for (int i = 0; i < pinCode.length(); ++i) {
nodes.append(buttons[QString(pinCode.at(i))]);
}
return nodes;
}
QList<Node> ScreenXmlParser::tryToFindPinCode(const QString &pinCode, const QString &title) {
for (const Node &node: m_nodes) {
// если там есть такая кнопка значит банер есть, и пин-код не прожать
if (node.text.trimmed() == "Отложить" && node.className == "android.widget.Button") {
return {};
}
}
for (const Node &node: m_nodes) {
// FIXME определение пин-кода лучше сделать
if (node.text.trimmed() == title) {
return findPinCodeNodes(pinCode);
}
}
return {};
}
QList<Node> ScreenXmlParser::getLast2DaysTransactions() {
QList<Node> nodes;
for (const Node &node: m_nodes) {
if (node.className == "android.widget.Button" && node.text.contains("рубл")
&& (node.text.contains("Минус") || node.text.contains("Плюс"))) {
nodes.append(node);
}
}
return nodes;
}
/**
* <node NAF="true" index="1" text="" resource-id=""
class="android.widget.Button" package="ru.rshb.dbo"
content-desc="" checkable="false" checked="false"
clickable="true" enabled="true" focusable="false"
focused="false" scrollable="false"
long-clickable="false" password="false"
selected="false" bounds="[952,159][1039,248]"/>
*/
void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
m_xml = xml;
m_nodes.clear();
QXmlStreamReader reader(xml);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name() == "node") {
Node node = Node();
if (reader.attributes().hasAttribute("bounds")) {
QString bounds = reader.attributes().value("bounds").toString();
if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) {
node.setBounds(
match.captured(1).toInt(),
match.captured(2).toInt(),
match.captured(3).toInt(),
match.captured(4).toInt()
);
}
}
if (reader.attributes().hasAttribute("text")) {
node.text = reader.attributes().value("text").toString();
}
if (reader.attributes().hasAttribute("class")) {
node.className = reader.attributes().value("class").toString();
}
if (reader.attributes().hasAttribute("clickable")) {
node.clickable = reader.attributes().value("clickable").toString() == "true";
}
if (reader.attributes().hasAttribute("focusable")) {
node.focusable = reader.attributes().value("focusable").toString() == "true";
}
if (reader.attributes().hasAttribute("resource-id")) {
node.resourceId = reader.attributes().value("resource-id").toString();
}
m_nodes.append(node);
}
}
}
void traverseNodes(const QDomNode &node) {
if (node.isElement()) {
QDomElement elem = node.toElement();
if (node.isElement()) {
QDomElement elem = node.toElement();
if (elem.tagName() == "node" &&
elem.attribute("class") == "android.widget.Button" &&
elem.attribute("text") == "Детали операции") {
// Ищем сиблингов от текущего узла и далее
QDomNodeList siblings = elem.parentNode().childNodes();
// Получение строки для анализа
// QString xmlString;
// QTextStream stream(&xmlString);
// elem.parentNode().save(stream, 0);
// qDebug() << xmlString;
// return;
for (int j = 0; j < siblings.count(); ++j) {
QDomNode sib = siblings.at(j);
if (!sib.isElement()) continue;
QDomElement sibElem = sib.toElement();
qDebug() << "Node:"
<< sibElem.attribute("index")
<< sibElem.attribute("text");
// << sibElem.attribute("class");
if (sibElem.hasChildNodes()) {
QDomNodeList sibChildren = sibElem.childNodes();
for (int k = 0; k < sibChildren.count(); ++k) {
QDomNode sibChild = sibChildren.at(k);
if (!sibChild.isElement()) continue;
QDomElement sibChildElem = sibChild.toElement();
qDebug() << "... Node:"
<< sibChildElem.attribute("index")
<< sibChildElem.attribute("text");
// << sibChildElem.attribute("class");
if (sibChildElem.hasChildNodes()) {
QDomNodeList sibChildren2 = sibChildElem.childNodes();
for (int k = 0; k < sibChildren2.count(); ++k) {
QDomNode sibChild2 = sibChildren2.at(k);
if (!sibChild2.isElement()) continue;
QDomElement sibChildElem2 = sibChild2.toElement();
qDebug() << "...... Node:"
<< sibChildElem2.attribute("index")
<< sibChildElem2.attribute("text");
// << sibChildElem2.attribute("class");
if (sibChildElem2.hasChildNodes()) {
QDomNodeList sibChildren3 = sibChildElem2.childNodes();
for (int k = 0; k < sibChildren3.count(); ++k) {
QDomNode sibChild3 = sibChildren3.at(k);
if (!sibChild3.isElement()) continue;
QDomElement sibChildElem3 = sibChild3.toElement();
qDebug() << "......... Node:"
<< sibChildElem3.attribute("index")
<< sibChildElem3.attribute("text");
// << sibChildElem2.attribute("class");
}
}
}
}
}
}
}
}
}
}
// Рекурсивно обходим всех детей
QDomNode child = node.firstChild();
while (!child.isNull()) {
traverseNodes(child);
child = child.nextSibling();
}
}
void ScreenXmlParser::test() {
QFile file("assets/rshb/pay/pay_by_phone_history_expanded.xml");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "Cannot open file";
}
QDomDocument doc;
if (!doc.setContent(&file)) {
qDebug() << "Failed to parse XML";
}
file.close();
qDebug() << "Start parsing XML:";
QDomElement root = doc.documentElement();
traverseNodes(root);
}