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
542fabf880
commit
1457064423
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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, секунда>.
|
||||||
|
|||||||
@ -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, секунда>.
|
||||||
|
|||||||
@ -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("Мои карты")) {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user