#include "ScreenXmlParser.h" #include #include #include #include #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 &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 {}; } Node ScreenXmlParser::findVerMasAfterTuActividad() { // "Ver más" идёт сразу после "Tu actividad" на домашнем экране bool afterTuActividad = false; for (const Node &node : m_nodes) { if (node.contentDesc == "Tu actividad") { afterTuActividad = true; continue; } if (afterTuActividad && node.contentDesc == QString::fromUtf8("Ver más") && node.clickable) { return node; } // Если встретили другую секцию — прекращаем поиск if (afterTuActividad && node.contentDesc == "Tarjetas") { break; } } return {}; } bool ScreenXmlParser::isTransactionLoadingScreen() { // Экран загрузки: заголовок "Tu Actividad" есть, но транзакций нет // Может содержать "Aún no tienes actividad" bool hasTuActividad = false; bool hasTransferencia = false; for (const Node &node : m_nodes) { if (node.contentDesc == "Tu Actividad") { hasTuActividad = true; } if (node.contentDesc.startsWith("Transferencia")) { hasTransferencia = true; } } return hasTuActividad && !hasTransferencia; } bool ScreenXmlParser::isTransactionListScreen() { // Экран транзакций определяется по заголовку "Tu Actividad" (с большой A) // + наличие транзакций (ImageView с "Transferencia") // На домашнем экране — "Tu actividad" (с маленькой a) bool hasTuActividad = false; bool hasTransferencia = false; for (const Node &node : m_nodes) { if (node.contentDesc == "Tu Actividad") { hasTuActividad = true; } if (node.contentDesc.startsWith("Transferencia")) { hasTransferencia = true; } } return hasTuActividad && hasTransferencia; } QList ScreenXmlParser::parseTransactionItems() { // Транзакции — ImageView с content-desc формата: // "Transferencia\n21 de febrero de 2026 - 09:39\n$-1.857,\n00" // "Transferencia externa de NOELIA ISABEL LEZCANO\n21 de febrero de 2026 - 09:35\n$123,\n45" QList result; for (const Node &node : m_nodes) { if (node.className != "android.widget.ImageView") continue; if (!node.contentDesc.startsWith("Transferencia")) continue; QStringList lines = node.contentDesc.split('\n'); if (lines.size() < 3) continue; TransactionInfo tx; // Строка 1: описание, может содержать имя отправителя // "Transferencia" или "Transferencia externa de NOELIA ISABEL LEZCANO" const QString descLine = lines[0].trimmed(); tx.description = descLine; if (descLine.contains("externa de ")) { tx.name = descLine.mid(descLine.indexOf("externa de ") + 11).trimmed(); } // Строка 2: дата "21 de febrero de 2026 - 09:39" tx.bankTime = lines[1].trimmed(); // Строки 3+: сумма "$-1.857,\n00" → объединяем QString amountStr; for (int i = 2; i < lines.size(); ++i) { amountStr += lines[i].trimmed(); } // "$-1.857,00" → убираем "$", "." (тысячный разделитель), заменяем "," на "." amountStr.remove('$'); amountStr.remove(' '); bool isNegative = amountStr.contains('-'); amountStr.remove('-'); amountStr.remove('.'); // тысячный разделитель amountStr.replace(',', '.'); bool ok = false; tx.amount = amountStr.toDouble(&ok); if (ok && isNegative) tx.amount = -tx.amount; tx.type = TransactionType::Card; result.append(tx); } return result; } QDateTime ScreenXmlParser::parseSpanishDate(const QString &dateStr) { // Формат: "21 de febrero de 2026 - 09:39" static const QMap spanishMonths = { {"enero", 1}, {"febrero", 2}, {"marzo", 3}, {"abril", 4}, {"mayo", 5}, {"junio", 6}, {"julio", 7}, {"agosto", 8}, {"septiembre", 9}, {"octubre", 10}, {"noviembre", 11}, {"diciembre", 12} }; QRegularExpression re(R"((\d{1,2})\s+de\s+(\w+)\s+de\s+(\d{4})\s*-\s*(\d{2}):(\d{2}))"); QRegularExpressionMatch match = re.match(dateStr); if (!match.hasMatch()) return {}; const int day = match.captured(1).toInt(); const QString monthName = match.captured(2).toLower(); const int year = match.captured(3).toInt(); const int hour = match.captured(4).toInt(); const int minute = match.captured(5).toInt(); const int month = spanishMonths.value(monthName, 0); if (month == 0) return {}; return QDateTime(QDate(year, month, day), QTime(hour, minute)); } bool ScreenXmlParser::isPayInputCardNumberScreen() { for (const Node &node : m_nodes) { if (node.hint == "Ingresar CBU, CVU o Alias") { return true; } } return false; } bool ScreenXmlParser::isPayTransferConfirmScreen() { // Экран с именем получателя, Alias/CBU и кнопкой "Transferir" // Но без "Vas a transferir" (это уже финальный экран) bool hasTransferir = false; bool hasVasATransferir = false; bool hasAlias = false; for (const Node &node : m_nodes) { if (node.contentDesc == "Transferir" && node.clickable) { hasTransferir = true; } if (node.contentDesc.startsWith("Vas a transferir")) { hasVasATransferir = true; } if (node.contentDesc.startsWith("Alias:")) { hasAlias = true; } } return hasTransferir && hasAlias && !hasVasATransferir; } bool ScreenXmlParser::isPayInputSumScreen() { for (const Node &node : m_nodes) { if (node.contentDesc.contains(QString::fromUtf8("Cuánto vas a transferír"))) { return true; } } return false; } bool ScreenXmlParser::isPayConfirmScreen() { for (const Node &node : m_nodes) { if (node.contentDesc.startsWith("Vas a transferir")) { return true; } } return false; } bool ScreenXmlParser::isPaySuccessScreen() { for (const Node &node : m_nodes) { if (node.contentDesc == "Dinero enviado!") { return true; } } return false; } QString ScreenXmlParser::parseRecipientNameFromConfirm() { // "Vas a transferir \na JOEL ARCHENTI" for (const Node &node : m_nodes) { if (node.contentDesc.startsWith("Vas a transferir")) { QStringList lines = node.contentDesc.split('\n'); if (lines.size() >= 2) { QString nameLine = lines[1].trimmed(); // Убираем "a " в начале if (nameLine.startsWith("a ")) { nameLine = nameLine.mid(2); } return nameLine.trimmed(); } } } return {}; } QString ScreenXmlParser::parsePaySuccessDateTime() { // Дата/время: "12/03/26 04:01" — содержит "/" и ":" for (const Node &node : m_nodes) { const QString desc = node.contentDesc.trimmed(); if (desc.contains('/') && desc.contains(':') && desc.length() <= 20) { return desc; } } return {}; } QString ScreenXmlParser::parsePaySuccessCoelsaId() { // "Id Coelsa" → следующий node содержит ID bool afterIdCoelsa = false; for (const Node &node : m_nodes) { if (node.contentDesc == "Id Coelsa") { afterIdCoelsa = true; continue; } if (afterIdCoelsa && !node.contentDesc.isEmpty()) { return node.contentDesc.trimmed(); } } return {}; } Node ScreenXmlParser::findEditTextByHint(const QString &hint) { for (const Node &node : m_nodes) { if (node.className == "android.widget.EditText" && node.hint == hint) { return node; } } return {}; } } // namespace Black