326 lines
12 KiB
C++
326 lines
12 KiB
C++
#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);
|
||
}
|