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:
parent
133e7d73d9
commit
7567ff74bf
@ -641,11 +641,9 @@ void GetLastTransactionsScript::doStart() {
|
||||
if (interrupted()) { m_error = "Interrupted"; emit finishedWithResult(m_error); return; }
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
|
||||
// 1) Старый blob-формат (до 3.2.33): "Успешный платеж" / "Поступление" —
|
||||
// одна нода с ключами и значениями через \n.
|
||||
for (const Node &n : xmlScreenParser.findAllNodes()) {
|
||||
// Принимаем оба формата детальной страницы:
|
||||
// — "Успешный платеж" (исходящий) с ключами "Дата:" / "Время:"
|
||||
// — "Поступление" (входящий) с ключами "Дата" / "Время"
|
||||
// Гейт — наличие даты И времени в любой форме.
|
||||
if (!n.contentDesc.contains(QString::fromUtf8("Дата"))) continue;
|
||||
if (!n.contentDesc.contains(QString::fromUtf8("Время"))) continue;
|
||||
const QStringList lines = n.contentDesc.split('\n');
|
||||
@ -667,6 +665,25 @@ void GetLastTransactionsScript::doStart() {
|
||||
detailLoaded = true;
|
||||
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;
|
||||
QThread::msleep(1500);
|
||||
}
|
||||
|
||||
@ -471,8 +471,9 @@ void PayByCardScript::makePayment(
|
||||
if (!m_resultScreenshot.isEmpty())
|
||||
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;
|
||||
double fee = 0;
|
||||
|
||||
@ -490,8 +491,19 @@ void PayByCardScript::makePayment(
|
||||
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 (без перехода в историю).
|
||||
// Уникальность за счёт <bankProfilePhone, секунда>.
|
||||
|
||||
@ -463,7 +463,8 @@ void PayByPhoneScript::makePayment(
|
||||
if (!m_resultScreenshot.isEmpty())
|
||||
emit screenshotReady(m_resultScreenshot);
|
||||
|
||||
// Парсим детали из content-desc "Платеж выполнен"
|
||||
// Парсим детали из экрана "Платеж выполнен". С 3.2.33 поля разнесены по
|
||||
// отдельным View-узлам — пробуем сначала старый blob-формат, потом fallback.
|
||||
QString bankTime;
|
||||
double fee = 0;
|
||||
|
||||
@ -481,8 +482,19 @@ void PayByPhoneScript::makePayment(
|
||||
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 (без перехода в историю).
|
||||
// Уникальность за счёт <bankProfilePhone, секунда>.
|
||||
|
||||
@ -174,6 +174,7 @@ bool ScreenXmlParser::isHomeScreen() {
|
||||
// основной странице приложения, а не на splash/PIN/диалоге.
|
||||
if (node.contentDesc == QString::fromUtf8("Переводы") ||
|
||||
node.contentDesc == QString::fromUtf8("Сервисы") ||
|
||||
node.contentDesc == QString::fromUtf8("MiniApps") ||
|
||||
node.contentDesc == QString::fromUtf8("История")) {
|
||||
hasBottomNav = true;
|
||||
}
|
||||
@ -243,12 +244,15 @@ bool ScreenXmlParser::isSideMenu() {
|
||||
bool hasVyhod = false;
|
||||
bool hasIdentified = false;
|
||||
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;
|
||||
}
|
||||
// На реальном экране content-desc = "Идентифицирован\nУспешно",
|
||||
// поэтому строгое равенство не проходит — используем startsWith.
|
||||
if (node.contentDesc.startsWith(QString::fromUtf8("Идентифицирован"))) {
|
||||
// До 3.2.33: "Идентифицирован\nУспешно" — отдельная нода (startsWith).
|
||||
// С 3.2.33: "Идентифицирован" вшито в блок профиля
|
||||
// ("IN\nIsroil Nazarov\n+992 ...\nИдентифицирован\nУспешно") — нужен contains.
|
||||
if (node.contentDesc.contains(QString::fromUtf8("Идентифицирован"))) {
|
||||
hasIdentified = true;
|
||||
}
|
||||
if (hasVyhod && hasIdentified) {
|
||||
@ -259,13 +263,22 @@ bool ScreenXmlParser::isSideMenu() {
|
||||
}
|
||||
|
||||
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) {
|
||||
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||||
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
|
||||
QStringList lines = node.contentDesc.split('\n');
|
||||
if (!lines.isEmpty()) {
|
||||
return lines[0].trimmed();
|
||||
const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован"));
|
||||
const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||||
node.contentDesc.contains(QString::fromUtf8("Сборка:"));
|
||||
if (!isNewProfileBlock && !isOldProfileBlock) continue;
|
||||
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() {
|
||||
static const QRegularExpression phoneRe(R"(^\+\d)");
|
||||
for (const Node &node : m_nodes) {
|
||||
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||||
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
|
||||
QStringList lines = node.contentDesc.split('\n');
|
||||
if (lines.size() >= 2) {
|
||||
return lines[1].trimmed();
|
||||
const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован"));
|
||||
const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||||
node.contentDesc.contains(QString::fromUtf8("Сборка:"));
|
||||
if (!isNewProfileBlock && !isOldProfileBlock) continue;
|
||||
for (const QString &line : node.contentDesc.split('\n')) {
|
||||
const QString trimmed = line.trimmed();
|
||||
if (phoneRe.match(trimmed).hasMatch()) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
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() {
|
||||
for (const Node &node : m_nodes) {
|
||||
if (node.contentDesc == QString::fromUtf8("Мои карты")) {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QDomElement>
|
||||
#include "android/xml/Node.h"
|
||||
@ -135,6 +136,12 @@ public:
|
||||
TransactionDetail parseTransactionDetail();
|
||||
|
||||
// Находит EditText по hint
|
||||
// Парсит пары "Лейбл: " → "Значение" для экранов, где поля разложены по
|
||||
// отдельным View-узлам в две колонки (формат с 3.2.33). Метод сканирует
|
||||
// все ноды: лейблом считается контент, оканчивающийся на ':', значением —
|
||||
// ближайший справа узел на той же Y-полосе. Возвращает map без двоеточий.
|
||||
QMap<QString, QString> parseLabelValuePairs();
|
||||
|
||||
Node findEditTextByHint(const QString &hint);
|
||||
|
||||
// Поле ввода суммы на экране перевода. У EditText'а нет hint/content-desc
|
||||
|
||||
Loading…
Reference in New Issue
Block a user