#include "ScreenXmlParser.h" #include #include #include #include #include #include #include #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()) { // 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 ScreenXmlParser::findPinCodeNodes(const QString &pinCode) { QMap buttons; const QSet 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 nodes; for (int i = 0; i < pinCode.length(); ++i) { nodes.append(buttons[QString(pinCode.at(i))]); } return nodes; } QList 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 ScreenXmlParser::getLast2DaysTransactions() { QList 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; } /** * */ 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); }