diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 560ac32..6206f13 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -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 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); } diff --git a/android/dushanbe/PayByCardScript.cpp b/android/dushanbe/PayByCardScript.cpp index bed290a..a0df50d 100644 --- a/android/dushanbe/PayByCardScript.cpp +++ b/android/dushanbe/PayByCardScript.cpp @@ -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 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 (без перехода в историю). // Уникальность за счёт . diff --git a/android/dushanbe/PayByPhoneScript.cpp b/android/dushanbe/PayByPhoneScript.cpp index c356b93..165ef2d 100644 --- a/android/dushanbe/PayByPhoneScript.cpp +++ b/android/dushanbe/PayByPhoneScript.cpp @@ -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 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 (без перехода в историю). // Уникальность за счёт . diff --git a/android/dushanbe/ScreenXmlParser.cpp b/android/dushanbe/ScreenXmlParser.cpp index a45020d..a49f71e 100644 --- a/android/dushanbe/ScreenXmlParser.cpp +++ b/android/dushanbe/ScreenXmlParser.cpp @@ -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 ScreenXmlParser::parseLabelValuePairs() { + // Применяется к экранам "Платеж выполнен" и подобным, где с 3.2.33 + // лейблы и значения сидят в отдельных View-нодах: лейбл слева + // (content-desc оканчивается ':'), значение — справа на той же Y-полосе. + QMap 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("Мои карты")) { diff --git a/android/dushanbe/ScreenXmlParser.h b/android/dushanbe/ScreenXmlParser.h index d089cc8..56bf00f 100644 --- a/android/dushanbe/ScreenXmlParser.h +++ b/android/dushanbe/ScreenXmlParser.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "android/xml/Node.h" @@ -135,6 +136,12 @@ public: TransactionDetail parseTransactionDetail(); // Находит EditText по hint + // Парсит пары "Лейбл: " → "Значение" для экранов, где поля разложены по + // отдельным View-узлам в две колонки (формат с 3.2.33). Метод сканирует + // все ноды: лейблом считается контент, оканчивающийся на ':', значением — + // ближайший справа узел на той же Y-полосе. Возвращает map без двоеточий. + QMap parseLabelValuePairs(); + Node findEditTextByHint(const QString &hint); // Поле ввода суммы на экране перевода. У EditText'а нет hint/content-desc