1
0
forked from BRT/arc

Improve GetLastTransactionsScript to parse bottom-sheet transaction details for version 3.2.33. Add parseDushanbeAmount for enhanced number extraction.

This commit is contained in:
trnsmkot 2026-05-21 17:29:49 +05:00
parent 8f7bc16090
commit 790b0b1847
3 changed files with 91 additions and 7 deletions

View File

@ -2,6 +2,7 @@
#include <QJsonDocument>
#include <QRegularExpression>
#include <QSet>
#include <QThread>
#include <QTimeZone>
@ -16,6 +17,24 @@
#include "AppLogger.h"
#include <QCoreApplication>
// Душанбе показывает суммы как "100,50", "100.50 TJS", "1 000,50",
// "1 000.50 TJS" и т.п. Голый QString::toDouble на таком вернёт 0,
// потому что требует, чтобы вся строка была валидным числом.
static double parseDushanbeAmount(const QString &raw) {
QString s = raw;
s.remove(QRegularExpression(R"([^\d,.\-])"));
s.replace(',', '.');
const int firstDot = s.indexOf('.');
if (firstDot >= 0) {
QString head = s.left(firstDot);
QString tail = s.mid(firstDot + 1);
tail.remove('.');
s = head + '.' + tail;
}
return s.toDouble();
}
static QString recognizeTextFromImage(const QByteArray &pngData) {
auto *api = new tesseract::TessBaseAPI();
if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) {
@ -649,9 +668,72 @@ 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.
// 1) Bottom-sheet формат (3.2.33, см. assets/dushanbe/3.2.33/tx_detail.xml):
// одна нода со всем content-desc через \n. Лейблы без двоеточий, сумма —
// отдельной строкой со знаком сразу после даты+времени.
// "Успешный платёж\n21.05.2026 16:58:32\n-10\nКарта\n...\nПолучатель\n...\nЗакрыть"
for (const Node &n : xmlScreenParser.findAllNodes()) {
const QString cd = n.contentDesc;
if (!cd.contains(QString::fromUtf8("Карта"))) continue;
if (!cd.contains(QString::fromUtf8("Получатель"))) continue;
if (!cd.contains(QString::fromUtf8("Операция"))) continue;
const QStringList lines = cd.split('\n', Qt::SkipEmptyParts);
if (lines.size() < 6) continue;
// lines[0] — заголовок: "Успешный платёж" / "Поступление" / ...
if (lines[0].contains(QString::fromUtf8("Успешн"))
|| lines[0].contains(QString::fromUtf8("Поступление"))
|| lines[0].contains(QString::fromUtf8("выполнен"))) {
detail.status = QString::fromUtf8("Успешный");
}
// lines[1] — "dd.MM.yyyy HH:mm:ss"
{
static const QRegularExpression reDt(
R"((\d{2}\.\d{2}\.\d{4})\s+(\d{2}:\d{2}:\d{2}))");
const auto m = reDt.match(lines[1]);
if (m.hasMatch()) {
detail.date = m.captured(1);
detail.time = m.captured(2);
}
}
// lines[2] — сумма со знаком: "-10" / "30" / "30.50" / "1 000,50".
detail.amount = parseDushanbeAmount(lines[2]);
// Дальше — пары label/value; значение может быть multi-line (Получатель).
// На сервер шлём первую строку получателя — таков сложившийся контракт.
static const QSet<QString> labels{
QString::fromUtf8("Карта"),
QString::fromUtf8("Получатель"),
QString::fromUtf8("Операция"),
QString::fromUtf8("Дата"),
QString::fromUtf8("Время"),
QString::fromUtf8("Закрыть"),
QString::fromUtf8("Назад"),
};
for (int i = 3; i < lines.size(); ++i) {
const QString lbl = lines[i].trimmed();
if (!labels.contains(lbl)) continue;
QStringList vals;
for (int j = i + 1; j < lines.size(); ++j) {
const QString cand = lines[j].trimmed();
if (labels.contains(cand)) break;
vals << cand;
}
if (vals.isEmpty()) continue;
if (lbl == QString::fromUtf8("Получатель")) {
detail.receiverAccount = vals.first();
} else if (lbl == QString::fromUtf8("Дата") && detail.date.isEmpty()) {
detail.date = vals.first();
} else if (lbl == QString::fromUtf8("Время") && detail.time.isEmpty()) {
detail.time = vals.first();
}
}
detailLoaded = true;
break;
}
// 2) Старый blob-формат (до 3.2.33): "Успешный платеж" / "Поступление" —
// одна нода с ключами и значениями через \n.
if (!detailLoaded) 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');
@ -663,7 +745,7 @@ void GetLastTransactionsScript::doStart() {
if (key == QString::fromUtf8("Получатель")) {
detail.receiverAccount = val;
} else if (key == QString::fromUtf8("Сумма")) {
detail.amount = val.toDouble();
detail.amount = parseDushanbeAmount(val);
} else if (key == QString::fromUtf8("Дата")) {
detail.date = val;
} else if (key == QString::fromUtf8("Время")) {
@ -673,7 +755,8 @@ void GetLastTransactionsScript::doStart() {
detailLoaded = true;
break;
}
// 2) Новый split-формат (с 3.2.33): лейблы и значения в отдельных View-нодах.
// 3) Split-формат: лейблы (с ':') и значения в отдельных View-нодах
// на одной Y-полосе. Промежуточная версия между blob и bottom-sheet.
if (!detailLoaded) {
const QMap<QString, QString> pairs = xmlScreenParser.parseLabelValuePairs();
const QString d = pairs.value(QString::fromUtf8("Дата"));
@ -682,11 +765,11 @@ void GetLastTransactionsScript::doStart() {
detail.date = d;
detail.time = t;
detail.receiverAccount = pairs.value(QString::fromUtf8("Получатель"));
detail.amount = pairs.value(QString::fromUtf8("Сумма")).toDouble();
detail.amount = parseDushanbeAmount(pairs.value(QString::fromUtf8("Сумма")));
if (detail.amount == 0.0) {
detail.amount = pairs.value(QString::fromUtf8("К зачислению")).toDouble();
detail.amount = parseDushanbeAmount(pairs.value(QString::fromUtf8("К зачислению")));
}
detail.fee = pairs.value(QString::fromUtf8("Комиссия")).toDouble();
detail.fee = parseDushanbeAmount(pairs.value(QString::fromUtf8("Комиссия")));
detail.operationId = pairs.value(QString::fromUtf8("Номер операции"));
detail.status = pairs.value(QString::fromUtf8("Статус"));
detailLoaded = true;

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

View File

@ -0,0 +1 @@
<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><hierarchy rotation="0"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.LinearLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="android:id/content" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.FrameLayout" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.widget.ImageView" package="tj.dc.next1" content-desc="Успешный платёж&#10;21.05.2026 16:58:32&#10;-10&#10;Карта&#10;9762 0002 2091 6042&#10;Получатель&#10;DCWallet*WDC0000**TAJIKISTAN&#10;992176827668-9762000191282739-&#10;Операция&#10;Дебетная часть P2P/операции&#10;Дата&#10;21.05.2026&#10;Время&#10;16:58:32&#10;Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="true" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]"><node index="0" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node><node index="1" text="" resource-id="" class="android.view.View" package="tj.dc.next1" content-desc="Закрыть" checkable="false" checked="false" clickable="true" enabled="true" focusable="true" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,0][720,1640]" /></node></node></node></node></node></node><node index="1" text="" resource-id="android:id/navigationBarBackground" class="android.view.View" package="tj.dc.next1" content-desc="" checkable="false" checked="false" clickable="false" enabled="true" focusable="false" focused="false" scrollable="false" long-clickable="false" password="false" selected="false" bounds="[0,1544][720,1640]" /></node></hierarchy>