1
0
forked from BRT/arc
arc/android/black/ScreenXmlParser.cpp
trnsmkot 449fb70386 Add GetLastTransactionsScript and PayByCardScript implementations for Black app
Implemented `GetLastTransactionsScript` for automated retrieval of transactions, handling home screen navigation, "Ver más" button detection, and transaction list parsing with database storage. Introduced `PayByCardScript` for automated card payments, covering recipient selection, amount input, payment confirmation, and transaction logging. Added XML assets and updates to support these workflows.
2026-03-12 18:42:38 +07:00

546 lines
17 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 <QMap>
#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 {};
}
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<TransactionInfo> 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<TransactionInfo> 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<QString, int> 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