1
0
forked from BRT/arc
arc/android/black/ScreenXmlParser.cpp
trnsmkot a7c23fabd1 Add core scripts for Black app automation and XML parsing
Implemented `CommonScript` as a base class with reusable functionality for automation workflows, including PIN input, app navigation, and server communication. Added specialized scripts like `GetCardInfoScript`, `GetProfileInfoScript`, and `LoginAndCheckAccountsScript` for card retrieval, profile parsing, and login validation. Introduced `ScreenXmlParser` for efficient XML-based UI parsing and node detection.
2026-03-11 19:45:08 +07:00

310 lines
9.0 KiB
C++

#include "ScreenXmlParser.h"
#include <QRegularExpression>
#include <QXmlStreamReader>
#include <QDomDocument>
#include "android/xml/Node.h"
namespace Black {
static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])");
ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
}
ScreenXmlParser::~ScreenXmlParser() = default;
void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
QDomDocument doc;
doc.setContent(xml);
m_xml = doc.documentElement();
m_nodes.clear();
QXmlStreamReader reader(xml);
while (!reader.atEnd()) {
reader.readNext();
if (reader.isStartElement() && reader.name() == u"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("content-desc")) {
node.contentDesc = reader.attributes().value("content-desc").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();
}
if (reader.attributes().hasAttribute("hint")) {
node.hint = reader.attributes().value("hint").toString();
}
if (reader.attributes().hasAttribute("password")) {
node.password = reader.attributes().value("password").toString() == "true";
}
m_nodes.append(node);
}
}
}
const QList<Node> &ScreenXmlParser::findAllNodes() const {
return m_nodes;
}
Node ScreenXmlParser::findTextNode(const QString &text) {
for (const Node &node : m_nodes) {
if (node.text.trimmed() == text) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) {
for (const Node &node : m_nodes) {
if (node.contentDesc == contentDesc) {
return node;
}
}
return {};
}
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc, bool contains) {
for (const Node &node : m_nodes) {
if (contains) {
if (node.contentDesc.contains(contentDesc)) {
return node;
}
} else {
if (node.contentDesc == contentDesc) {
return node;
}
}
}
return {};
}
Node ScreenXmlParser::findButtonNode(const QString &text, bool contains) {
for (const Node &node : m_nodes) {
if (!node.clickable) continue;
if (contains) {
if (node.text.contains(text) || node.contentDesc.contains(text)) {
return node;
}
} else {
if (node.text.trimmed() == text || node.contentDesc == text) {
return node;
}
}
}
return {};
}
Node ScreenXmlParser::findFirstEditText() {
for (const Node &node : m_nodes) {
if (node.className == "android.widget.EditText") {
return node;
}
}
return {};
}
bool ScreenXmlParser::isPinCodeScreen() {
for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("¿Olvidaste tu PIN?"))) {
return true;
}
if (node.contentDesc.contains(QString::fromUtf8("Ingresá tu PIN"))) {
return true;
}
}
return false;
}
Node ScreenXmlParser::findPinCodeEditText() {
for (const Node &node : m_nodes) {
if (node.className == "android.widget.EditText" && node.password) {
return node;
}
}
return {};
}
bool ScreenXmlParser::isHomeScreen() {
bool hasSaldo = false;
bool hasInicio = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == "Saldo disponible") {
hasSaldo = true;
}
if (node.contentDesc == "Inicio") {
hasInicio = true;
}
if (hasSaldo && hasInicio) {
return true;
}
}
return false;
}
bool ScreenXmlParser::isSideMenu() {
bool hasTuInfo = false;
bool hasCerrar = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Tu Información")) {
hasTuInfo = true;
}
if (node.contentDesc == QString::fromUtf8("Cerrar sesión")) {
hasCerrar = true;
}
if (hasTuInfo && hasCerrar) {
return true;
}
}
return false;
}
Node ScreenXmlParser::findHolaButton() {
for (const Node &node : m_nodes) {
if (node.clickable && node.contentDesc.startsWith(QString::fromUtf8("¡Hola!"))) {
return node;
}
}
return {};
}
QString ScreenXmlParser::parseUserNameFromHomeScreen() {
// content-desc: "¡Hola!\nveronica cintia heredia \nvc"
for (const Node &node : m_nodes) {
if (node.contentDesc.startsWith(QString::fromUtf8("¡Hola!"))) {
QStringList lines = node.contentDesc.split('\n');
if (lines.size() >= 2) {
return lines[1].trimmed();
}
}
}
return {};
}
double ScreenXmlParser::parseBalanceFromHomeScreen() {
// Баланс идёт после "Saldo disponible", формат: "$1.857,00" или "$0,00"
bool afterSaldo = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == "Saldo disponible") {
afterSaldo = true;
continue;
}
if (afterSaldo && node.contentDesc.startsWith("$")) {
// "$1.857,00" → "1857.00"
QString raw = node.contentDesc.mid(1); // убираем "$"
raw.remove('.'); // убираем разделитель тысяч
raw.replace(',', '.'); // запятая → точка
bool ok = false;
double val = raw.toDouble(&ok);
if (ok) return val;
}
}
return 0.0;
}
bool ScreenXmlParser::isProfilePage() {
for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Tu información")) {
return true;
}
}
return false;
}
QString ScreenXmlParser::parseProfileNombre() {
for (const Node &node : m_nodes) {
if (node.hint == "Nombre" && !node.text.isEmpty()) {
return node.text.trimmed();
}
}
return {};
}
QString ScreenXmlParser::parseProfileApellido() {
for (const Node &node : m_nodes) {
if (node.hint == "Apellido" && !node.text.isEmpty()) {
return node.text.trimmed();
}
}
return {};
}
QString ScreenXmlParser::parseProfileEmail() {
for (const Node &node : m_nodes) {
if (node.hint == "E-mail" && !node.text.isEmpty()) {
return node.text.trimmed();
}
}
return {};
}
QString ScreenXmlParser::parseProfilePhone() {
for (const Node &node : m_nodes) {
if (node.hint == QString::fromUtf8("Teléfono celular") && !node.text.isEmpty()) {
return node.text.trimmed();
}
}
return {};
}
bool ScreenXmlParser::isCVUScreen() {
for (const Node &node : m_nodes) {
if (node.contentDesc == "CVU") {
return true;
}
}
return false;
}
QString ScreenXmlParser::parseCVUNumber() {
// CVU номер идёт после content-desc="CVU", это 22-значный номер
bool afterCVU = false;
for (const Node &node : m_nodes) {
if (node.contentDesc == "CVU") {
afterCVU = true;
continue;
}
if (afterCVU && !node.contentDesc.isEmpty()) {
// Проверяем что это числовая строка (CVU — 22 цифры)
QString cvu = node.contentDesc.trimmed();
bool allDigits = true;
for (const QChar &c : cvu) {
if (!c.isDigit()) {
allDigits = false;
break;
}
}
if (allDigits && !cvu.isEmpty()) {
return cvu;
}
}
}
return {};
}
} // namespace Black