1
0
forked from BRT/arc

Add support for split formats (v3.2.33+) in payment details parsing, fallback logic for older formats, and XML label-value pair extraction. Update ScreenXmlParser for enhanced node parsing.

This commit is contained in:
slava 2026-05-19 11:48:58 +05:00
parent 542fabf880
commit 1457064423
5 changed files with 120 additions and 24 deletions

View File

@ -641,11 +641,9 @@ void GetLastTransactionsScript::doStart() {
if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; } if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; }
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
// 1) Старый blob-формат (до 3.2.33): "Успешный платеж" / "Поступление" —
// одна нода с ключами и значениями через \n.
for (const Node &n : xmlScreenParser.findAllNodes()) { for (const Node &n : xmlScreenParser.findAllNodes()) {
// Принимаем оба формата детальной страницы:
// — "Успешный платеж" (исходящий) с ключами "Дата:" / "Время:"
// — "Поступление" (входящий) с ключами "Дата" / "Время"
// Гейт — наличие даты И времени в любой форме.
if (!n.contentDesc.contains(QString::fromUtf8("Дата"))) continue; if (!n.contentDesc.contains(QString::fromUtf8("Дата"))) continue;
if (!n.contentDesc.contains(QString::fromUtf8("Время"))) continue; if (!n.contentDesc.contains(QString::fromUtf8("Время"))) continue;
const QStringList lines = n.contentDesc.split('\n'); const QStringList lines = n.contentDesc.split('\n');
@ -667,6 +665,25 @@ void GetLastTransactionsScript::doStart() {
detailLoaded = true; detailLoaded = true;
break; break;
} }
// 2) Новый split-формат (с 3.2.33): лейблы и значения в отдельных View-нодах.
if (!detailLoaded) {
const QMap<QString, QString> pairs = xmlScreenParser.parseLabelValuePairs();
const QString d = pairs.value(QString::fromUtf8("Дата"));
const QString t = pairs.value(QString::fromUtf8("Время"));
if (!d.isEmpty() && !t.isEmpty()) {
detail.date = d;
detail.time = t;
detail.receiverAccount = pairs.value(QString::fromUtf8("Получатель"));
detail.amount = pairs.value(QString::fromUtf8("Сумма")).toDouble();
if (detail.amount == 0.0) {
detail.amount = pairs.value(QString::fromUtf8("К зачислению")).toDouble();
}
detail.fee = pairs.value(QString::fromUtf8("Комиссия")).toDouble();
detail.operationId = pairs.value(QString::fromUtf8("Номер операции"));
detail.status = pairs.value(QString::fromUtf8("Статус"));
detailLoaded = true;
}
}
if (detailLoaded) break; if (detailLoaded) break;
QThread::msleep(1500); QThread::msleep(1500);
} }

View File

@ -471,8 +471,9 @@ void PayByCardScript::makePayment(
if (!m_resultScreenshot.isEmpty()) if (!m_resultScreenshot.isEmpty())
emit screenshotReady(m_resultScreenshot); emit screenshotReady(m_resultScreenshot);
// Парсим детали из content-desc "Платеж выполнен" // Парсим детали из экрана "Платеж выполнен".
// Формат: "Платеж выполнен\nПоставщик:\n...\nКомиссия:\n0.00\nДата:\n05.04.26\nВремя:\n20:19:56\nНа главную" // До 3.2.33 все поля шли одним content-desc с ключами "Дата:" / "Время:" / "Комиссия:".
// С 3.2.33 лейблы и значения — отдельные View-ноды в двух колонках.
QString bankTime; QString bankTime;
double fee = 0; double fee = 0;
@ -490,8 +491,19 @@ void PayByCardScript::makePayment(
bankTime += " " + lines[i + 1].trimmed(); bankTime += " " + lines[i + 1].trimmed();
} }
} }
qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee;
} }
if (bankTime.isEmpty()) {
const QMap<QString, QString> pairs = xmlScreenParser.parseLabelValuePairs();
const QString d = pairs.value(QString::fromUtf8("Дата"));
const QString t = pairs.value(QString::fromUtf8("Время"));
if (!d.isEmpty() || !t.isEmpty()) {
bankTime = (d + " " + t).trimmed();
}
if (fee == 0) {
fee = pairs.value(QString::fromUtf8("Комиссия")).toDouble();
}
}
qDebug() << "[Dushanbe::PayByCard] Result: date=" << bankTime << "fee=" << fee;
// bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю). // bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю).
// Уникальность за счёт <bankProfilePhone, секунда>. // Уникальность за счёт <bankProfilePhone, секунда>.

View File

@ -463,7 +463,8 @@ void PayByPhoneScript::makePayment(
if (!m_resultScreenshot.isEmpty()) if (!m_resultScreenshot.isEmpty())
emit screenshotReady(m_resultScreenshot); emit screenshotReady(m_resultScreenshot);
// Парсим детали из content-desc "Платеж выполнен" // Парсим детали из экрана "Платеж выполнен". С 3.2.33 поля разнесены по
// отдельным View-узлам — пробуем сначала старый blob-формат, потом fallback.
QString bankTime; QString bankTime;
double fee = 0; double fee = 0;
@ -481,8 +482,19 @@ void PayByPhoneScript::makePayment(
bankTime += " " + lines[i + 1].trimmed(); bankTime += " " + lines[i + 1].trimmed();
} }
} }
qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee;
} }
if (bankTime.isEmpty()) {
const QMap<QString, QString> pairs = xmlScreenParser.parseLabelValuePairs();
const QString d = pairs.value(QString::fromUtf8("Дата"));
const QString t = pairs.value(QString::fromUtf8("Время"));
if (!d.isEmpty() || !t.isEmpty()) {
bankTime = (d + " " + t).trimmed();
}
if (fee == 0) {
fee = pairs.value(QString::fromUtf8("Комиссия")).toDouble();
}
}
qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee;
// bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю). // bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю).
// Уникальность за счёт <bankProfilePhone, секунда>. // Уникальность за счёт <bankProfilePhone, секунда>.

View File

@ -174,6 +174,7 @@ bool ScreenXmlParser::isHomeScreen() {
// основной странице приложения, а не на splash/PIN/диалоге. // основной странице приложения, а не на splash/PIN/диалоге.
if (node.contentDesc == QString::fromUtf8("Переводы") || if (node.contentDesc == QString::fromUtf8("Переводы") ||
node.contentDesc == QString::fromUtf8("Сервисы") || node.contentDesc == QString::fromUtf8("Сервисы") ||
node.contentDesc == QString::fromUtf8("MiniApps") ||
node.contentDesc == QString::fromUtf8("История")) { node.contentDesc == QString::fromUtf8("История")) {
hasBottomNav = true; hasBottomNav = true;
} }
@ -243,12 +244,15 @@ bool ScreenXmlParser::isSideMenu() {
bool hasVyhod = false; bool hasVyhod = false;
bool hasIdentified = false; bool hasIdentified = false;
for (const Node &node : m_nodes) { for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Выход")) { // До 3.2.33 кнопка была "Выход", с 3.2.33 — "Выйти".
if (node.contentDesc == QString::fromUtf8("Выход") ||
node.contentDesc == QString::fromUtf8("Выйти")) {
hasVyhod = true; hasVyhod = true;
} }
// На реальном экране content-desc = "Идентифицирован\nУспешно", // До 3.2.33: "Идентифицирован\nУспешно" — отдельная нода (startsWith).
// поэтому строгое равенство не проходит — используем startsWith. // С 3.2.33: "Идентифицирован" вшито в блок профиля
if (node.contentDesc.startsWith(QString::fromUtf8("Идентифицирован"))) { // ("IN\nIsroil Nazarov\n+992 ...\nИдентифицирован\nУспешно") — нужен contains.
if (node.contentDesc.contains(QString::fromUtf8("Идентифицирован"))) {
hasIdentified = true; hasIdentified = true;
} }
if (hasVyhod && hasIdentified) { if (hasVyhod && hasIdentified) {
@ -259,13 +263,22 @@ bool ScreenXmlParser::isSideMenu() {
} }
QString ScreenXmlParser::parseProfileFullName() { QString ScreenXmlParser::parseProfileFullName() {
// content-desc: "Мехрубон Имомкулзода\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443" // До 3.2.33 одна нода содержит:
// "Имя Фамилия\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443"
// С 3.2.33 "Версия"/"Сборка" вынесены в отдельные ноды, а профиль:
// "IN\nIsroil Nazarov\n+992 (17) 682-7668\nИдентифицирован\nУспешно"
// Находим ноду по наличию "Идентифицирован" ИЛИ "Версия:+Сборка:", затем
// выбираем строку, идущую непосредственно перед строкой с телефоном "+...".
static const QRegularExpression phoneRe(R"(^\+\d)");
for (const Node &node : m_nodes) { for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) && const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован"));
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) { const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
QStringList lines = node.contentDesc.split('\n'); node.contentDesc.contains(QString::fromUtf8("Сборка:"));
if (!lines.isEmpty()) { if (!isNewProfileBlock && !isOldProfileBlock) continue;
return lines[0].trimmed(); const QStringList lines = node.contentDesc.split('\n');
for (int i = 0; i < lines.size(); ++i) {
if (phoneRe.match(lines[i].trimmed()).hasMatch() && i > 0) {
return lines[i - 1].trimmed();
} }
} }
} }
@ -273,18 +286,53 @@ QString ScreenXmlParser::parseProfileFullName() {
} }
QString ScreenXmlParser::parseProfilePhone() { QString ScreenXmlParser::parseProfilePhone() {
static const QRegularExpression phoneRe(R"(^\+\d)");
for (const Node &node : m_nodes) { for (const Node &node : m_nodes) {
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) && const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован"));
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) { const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
QStringList lines = node.contentDesc.split('\n'); node.contentDesc.contains(QString::fromUtf8("Сборка:"));
if (lines.size() >= 2) { if (!isNewProfileBlock && !isOldProfileBlock) continue;
return lines[1].trimmed(); for (const QString &line : node.contentDesc.split('\n')) {
const QString trimmed = line.trimmed();
if (phoneRe.match(trimmed).hasMatch()) {
return trimmed;
} }
} }
} }
return {}; return {};
} }
QMap<QString, QString> ScreenXmlParser::parseLabelValuePairs() {
// Применяется к экранам "Платеж выполнен" и подобным, где с 3.2.33
// лейблы и значения сидят в отдельных View-нодах: лейбл слева
// (content-desc оканчивается ':'), значение — справа на той же Y-полосе.
QMap<QString, QString> result;
for (int i = 0; i < m_nodes.size(); ++i) {
const QString rawLabel = m_nodes[i].contentDesc.trimmed();
if (rawLabel.isEmpty() || !rawLabel.endsWith(':')) continue;
const QString label = rawLabel.left(rawLabel.size() - 1).trimmed();
if (label.isEmpty() || result.contains(label)) continue;
const Node &lbl = m_nodes[i];
int bestIdx = -1;
for (int j = 0; j < m_nodes.size(); ++j) {
if (j == i) continue;
const QString candidate = m_nodes[j].contentDesc.trimmed();
if (candidate.isEmpty()) continue;
if (candidate.endsWith(':')) continue;
if (m_nodes[j].x1() < lbl.x2()) continue;
if (qAbs(m_nodes[j].y1() - lbl.y1()) > 8) continue;
if (bestIdx < 0 || m_nodes[j].x1() < m_nodes[bestIdx].x1()) {
bestIdx = j;
}
}
if (bestIdx >= 0) {
result[label] = m_nodes[bestIdx].contentDesc.trimmed();
}
}
return result;
}
bool ScreenXmlParser::isCardsScreen() { bool ScreenXmlParser::isCardsScreen() {
for (const Node &node : m_nodes) { for (const Node &node : m_nodes) {
if (node.contentDesc == QString::fromUtf8("Мои карты")) { if (node.contentDesc == QString::fromUtf8("Мои карты")) {

View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <QMap>
#include <QObject> #include <QObject>
#include <QDomElement> #include <QDomElement>
#include "android/xml/Node.h" #include "android/xml/Node.h"
@ -135,6 +136,12 @@ public:
TransactionDetail parseTransactionDetail(); TransactionDetail parseTransactionDetail();
// Находит EditText по hint // Находит EditText по hint
// Парсит пары "Лейбл: " → "Значение" для экранов, где поля разложены по
// отдельным View-узлам в две колонки (формат с 3.2.33). Метод сканирует
// все ноды: лейблом считается контент, оканчивающийся на ':', значением —
// ближайший справа узел на той же Y-полосе. Возвращает map без двоеточий.
QMap<QString, QString> parseLabelValuePairs();
Node findEditTextByHint(const QString &hint); Node findEditTextByHint(const QString &hint);
// Поле ввода суммы на экране перевода. У EditText'а нет hint/content-desc // Поле ввода суммы на экране перевода. У EditText'а нет hint/content-desc