1
0
forked from BRT/arc
arc/android/dushanbe/PayByPhoneScript.cpp
trnsmkot 4694e6db50 - Add findSumEditText to ScreenXmlParser for enhanced sum field detection.
- Update `PayByPhoneScript` and `PayByCardScript` to use `findSumEditText` as a fallback.
- Enhance `FileLogWindow` to export logs and database files to backend via `NetworkService`.
- Add `networkService` getter in `TaskDumpRecorder` for centralizing access to `NetworkService`.
2026-05-07 22:54:06 +07:00

521 lines
22 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "PayByPhoneScript.h"
#include <QRegularExpression>
#include <QThread>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dump/TaskDumpRecorder.h"
#include "ocr/OcrUtils.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "AppLogger.h"
#include <QCoreApplication>
static QString recognizeOperationId(const QByteArray &pngData) {
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) return {};
const int w = pixGetWidth(pix);
const int h = pixGetHeight(pix);
auto *api = new tesseract::TessBaseAPI();
if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) {
pixDestroy(&pix); delete api;
return {};
}
api->SetImage(pix);
api->Recognize(nullptr);
int opLineY = -1, opLineH = 0;
bool prevWasNomer = false;
tesseract::ResultIterator *ri = api->GetIterator();
if (ri) {
do {
const char *word = ri->GetUTF8Text(tesseract::RIL_WORD);
if (word) {
const QString w_str = QString::fromUtf8(word);
delete[] word;
if (w_str.contains(QString::fromUtf8("омер")) ||
w_str.contains(QString::fromUtf8("omep"))) {
prevWasNomer = true;
continue;
}
if (prevWasNomer &&
(w_str.contains(QString::fromUtf8("операции")) ||
w_str.contains(QString::fromUtf8("onepaции")))) {
int x1, y1, x2, y2;
ri->BoundingBox(tesseract::RIL_WORD, &x1, &y1, &x2, &y2);
opLineY = y1;
opLineH = y2 - y1;
break;
}
prevWasNomer = false;
}
} while (ri->Next(tesseract::RIL_WORD));
}
api->End();
delete api;
if (opLineY < 0) { pixDestroy(&pix); return {}; }
const int cropY = qMax(0, opLineY - opLineH / 2);
const int cropH = opLineH * 2;
BOX *box = boxCreate(w / 2, cropY, w / 2, qMin(cropH, h - cropY));
Pix *cropped = pixClipRectangle(pix, box, nullptr);
boxDestroy(&box);
pixDestroy(&pix);
if (!cropped) return {};
auto *api2 = new tesseract::TessBaseAPI();
if (api2->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "eng")) {
pixDestroy(&cropped); delete api2;
return {};
}
api2->SetVariable("tessedit_char_whitelist", "0123456789");
api2->SetPageSegMode(tesseract::PSM_SINGLE_LINE);
api2->SetImage(cropped);
char *text = api2->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&cropped);
api2->End();
delete api2;
result.remove(QRegularExpression("[^0-9]"));
if (result.length() >= 8) {
qDebug() << "[Dushanbe::OCR] Precise operation ID:" << result;
return result;
}
return {};
}
namespace Dushanbe {
PayByPhoneScript::PayByPhoneScript(
MaterialInfo account,
QString phone,
double amount,
QObject *parent
) : CommonScript(parent),
m_account(std::move(account)),
m_phone(std::move(phone)),
m_amount(amount) {
}
PayByPhoneScript::~PayByPhoneScript() = default;
void PayByPhoneScript::doStart() {
const DeviceInfo device = DeviceDAO::getDeviceById(m_account.deviceId);
const BankProfileInfo app = BankProfileDAO::getApplication(m_account.deviceId, m_account.appCode);
if (device.id.isEmpty() || app.id == -1) {
m_error = "Device or app not found: " + m_account.deviceId;
qCritical() << m_error;
emit finishedWithResult(m_error);
return;
}
const TaskDumpMeta dumpMeta{m_account.materialId, m_amount, m_phone, {}};
TaskDumpRecorder::Scope dumpScope("dushanbe", "CREATE_TRANSACTION_PHONE", device.adbSerial, dumpMeta, &m_error);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[Dushanbe::PayByPhone] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(device.adbSerial, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
qWarning() << m_error;
AppLogger::log("dushanbe/PayByPhone", device.id, m_error, {}, "CREATE_TRANSACTION");
AdbUtils::tryToKillApplication(device.adbSerial, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(device.adbSerial, ++m_counter));
}
makePayment(device.adbSerial, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &pinCode,
const QString &bankProfilePhone,
const int width,
const int height
) {
// 1. Тапаем "Переводы" в нижней навигации
Node transfersTab = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Переводы"));
if (transfersTab.isEmpty()) {
m_error = "Tab 'Переводы' not found on home screen";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Переводы' tab";
AdbUtils::makeTap(deviceId, transfersTab.x(), transfersTab.y());
QThread::msleep(2000);
// 2. Ждём экран переводов — ищем "DC (по номеру телефона)"
bool transfersLoaded = false;
Node phoneTransferBtn;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
phoneTransferBtn = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("DC (по номеру телефона)"), true);
if (!phoneTransferBtn.isEmpty()) {
transfersLoaded = true;
break;
}
qDebug() << "[Dushanbe::PayByPhone] Waiting for transfers screen, attempt" << attempt + 1;
QThread::msleep(1500);
}
if (!transfersLoaded) {
m_error = "Transfers screen not loaded or 'DC (по номеру телефона)' not found";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 3. Тапаем "DC (по номеру телефона)"
qDebug() << "[Dushanbe::PayByPhone] Tapping 'DC (по номеру телефона)'";
AdbUtils::makeTap(deviceId, phoneTransferBtn.x(), phoneTransferBtn.y());
QThread::msleep(2000);
// 4. Ждём экран ввода — ищем EditText с hint "Номер телефона получателя"
bool inputScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
Node editText = xmlScreenParser.findEditTextByHint(
QString::fromUtf8("Номер телефона получателя"));
if (editText.isEmpty()) {
editText = xmlScreenParser.findFirstEditText();
}
if (!editText.isEmpty()) {
inputScreenLoaded = true;
break;
}
qDebug() << "[Dushanbe::PayByPhone] Waiting for phone input screen, attempt" << attempt + 1;
QThread::msleep(1500);
}
if (!inputScreenLoaded) {
m_error = "Phone input screen not loaded";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 5. Вводим номер телефона
Node phoneEditText = xmlScreenParser.findEditTextByHint(
QString::fromUtf8("Номер телефона получателя"));
if (phoneEditText.isEmpty()) {
phoneEditText = xmlScreenParser.findFirstEditText();
}
AdbUtils::makeTap(deviceId, phoneEditText.x(), phoneEditText.y());
QThread::msleep(500);
// Убираем "+" из номера если есть — вводим только цифры
QString phoneDigits = m_phone;
phoneDigits.remove('+');
AdbUtils::inputText(deviceId, phoneDigits);
QThread::msleep(1000);
// Скрываем клавиатуру чтобы поле суммы стало видимым
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
// 6. Вводим сумму в поле "Сумма"
Node sumEditText;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
sumEditText = xmlScreenParser.findEditTextByHint(QString::fromUtf8("Сумма"));
if (sumEditText.isEmpty()) sumEditText = xmlScreenParser.findSumEditText();
if (!sumEditText.isEmpty()) break;
qDebug() << "[Dushanbe::PayByPhone] Waiting for sum input, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (sumEditText.isEmpty()) {
m_error = "Sum input field not found";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
AdbUtils::makeTap(deviceId, sumEditText.x(), sumEditText.y());
QThread::msleep(500);
const QString amountStr = (m_amount == static_cast<int>(m_amount))
? QString::number(static_cast<int>(m_amount))
: QString::number(m_amount, 'f', 2);
AdbUtils::inputText(deviceId, amountStr);
QThread::msleep(1000);
// 6a. Скрываем клавиатуру и выбираем карту-источник (под полем "Комментарий")
AdbUtils::goBack(deviceId);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
{
const QString cardMask = "****" + m_account.lastNumbers;
Node cardNode;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (!n.clickable) continue;
if (n.contentDesc.contains(cardMask)) { cardNode = n; break; }
}
if (cardNode.isEmpty()) {
m_error = "Source card not found: " + cardMask;
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByPhone] Selecting source card:" << cardMask;
AdbUtils::makeTap(deviceId, cardNode.x(), cardNode.y());
QThread::msleep(500);
}
// 7. Нажимаем "Далее"
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node daleeBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Далее"));
if (daleeBtn.isEmpty()) {
daleeBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Далее"), false);
}
if (daleeBtn.isEmpty()) {
m_error = "Button 'Далее' not found";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Далее'";
AdbUtils::makeTap(deviceId, daleeBtn.x(), daleeBtn.y());
QThread::msleep(2000);
// 8. После "Далее" кнопка меняется на "Оплатить"
bool oplatitFound = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
Node oplatitBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Оплатить"));
if (oplatitBtn.isEmpty()) {
oplatitBtn = xmlScreenParser.findButtonNode(QString::fromUtf8("Оплатить"), false);
}
if (!oplatitBtn.isEmpty()) {
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Оплатить'";
AdbUtils::makeTap(deviceId, oplatitBtn.x(), oplatitBtn.y());
oplatitFound = true;
QThread::msleep(3000);
break;
}
qDebug() << "[Dushanbe::PayByPhone] Waiting for 'Оплатить', attempt" << attempt + 1;
QThread::msleep(1500);
}
if (!oplatitFound) {
m_error = "Button 'Оплатить' not found";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 9. Ждём bottom sheet "Подтверждение платежа" и тапаем "Подтверждаю"
bool confirmed = false;
for (int attempt = 0; attempt < 10; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
if (xmlScreenParser.isPinCodeScreen()) {
qDebug() << "[Dushanbe::PayByPhone] PIN code screen, entering PIN...";
inputPinCode(deviceId, pinCode);
QThread::msleep(3000);
continue;
}
Node confirmNode = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Подтверждение платежа"), true);
if (!confirmNode.isEmpty() &&
confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
// Compose flatten-ит весь bottom sheet в одну accessibility-кнопку,
// отдельного узла "Подтверждаю" в дампе нет — координаты берём OCR-ом
// по скриншоту. Если OCR не нашёл — fallback на геометрию (центр
// нижней четверти sheet, выше системного nav-бара).
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
QPoint tap = OcrUtils::findTextCenter(screen,
QString::fromUtf8("Подтверждаю"),
confirmNode.y1(), confirmNode.y2());
if (tap.x() < 0) {
const int tapY = confirmNode.y2() - qMax(150, height / 14);
tap = QPoint(confirmNode.x(), tapY);
qDebug() << "[Dushanbe::PayByPhone] OCR miss, fallback tap at" << tap;
}
qDebug() << "[Dushanbe::PayByPhone] Tapping 'Подтверждаю' at" << tap;
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
confirmed = true;
QThread::msleep(4000);
break;
}
qDebug() << "[Dushanbe::PayByPhone] Waiting for confirmation sheet, attempt" << attempt + 1;
QThread::msleep(2000);
}
if (!confirmed) {
m_error = "Confirmation sheet not found";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
// 10. Ждём результат — "Платеж выполнен" или ошибку.
// Промежуточный статус "Выполняется" может висеть до ~1.5 мин, пока банк
// подтверждает перевод, поэтому общий таймаут — 60 итераций × 2 сек = 2 мин.
bool success = false;
for (int attempt = 0; attempt < 60; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
if (xmlScreenParser.isPinCodeScreen()) {
qDebug() << "[Dushanbe::PayByPhone] PIN code screen (retry), entering PIN...";
inputPinCode(deviceId, pinCode);
QThread::msleep(3000);
continue;
}
if (!xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Платеж выполнен"), true).isEmpty()) {
success = true;
break;
}
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка"), true).isEmpty() ||
!xmlScreenParser.findTextNode(QString::fromUtf8("Ошибка")).isEmpty()) {
m_error = "Transfer failed: error screen detected";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
if (!xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Выполняется"), true).isEmpty()) {
qDebug() << "[Dushanbe::PayByPhone] Status 'Выполняется', waiting for completion, attempt"
<< attempt + 1;
QThread::msleep(2000);
continue;
}
// Если bottom sheet "Подтверждение платежа" всё ещё виден — первый тап
// не сработал (мимо OCR-координат), повторяем его той же логикой.
Node retryConfirm = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Подтверждение платежа"), true);
if (!retryConfirm.isEmpty() &&
retryConfirm.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
qDebug() << "[Dushanbe::PayByPhone] Confirm sheet still visible, re-tapping 'Подтверждаю'";
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
QPoint tap = OcrUtils::findTextCenter(screen,
QString::fromUtf8("Подтверждаю"),
retryConfirm.y1(), retryConfirm.y2());
if (tap.x() < 0) {
const int tapY = retryConfirm.y2() - qMax(150, height / 14);
tap = QPoint(retryConfirm.x(), tapY);
}
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
QThread::msleep(4000);
continue;
}
qDebug() << "[Dushanbe::PayByPhone] Waiting for result, attempt" << attempt + 1;
QThread::msleep(2000);
}
if (!success) {
m_error = "Payment result not received within timeout";
qWarning() << "[Dushanbe::PayByPhone]" << m_error;
AppLogger::log("dushanbe/PayByPhone", deviceId, m_error,
AdbUtils::captureScreenshotBytes(deviceId), "CREATE_TRANSACTION");
return;
}
qDebug() << "[Dushanbe::PayByPhone] Payment successful!";
m_error.clear();
// Скриншот чека — сохраняем как m_resultScreenshot и отправляем в мониторинг
m_resultScreenshot = AdbUtils::captureScreenshotBytes(deviceId);
const QByteArray &receiptImage = m_resultScreenshot;
if (!m_resultScreenshot.isEmpty())
emit screenshotReady(m_resultScreenshot);
// Парсим детали из content-desc "Платеж выполнен"
QString bankTime;
double fee = 0;
Node resultNode = xmlScreenParser.findNodeByContentDesc(
QString::fromUtf8("Платеж выполнен"), true);
if (!resultNode.isEmpty()) {
const QStringList lines = resultNode.contentDesc.split('\n');
for (int i = 0; i < lines.size(); ++i) {
const QString line = lines[i].trimmed();
if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
fee = lines[i + 1].trimmed().toDouble();
} else if (line.startsWith(QString::fromUtf8("Дата:")) && i + 1 < lines.size()) {
bankTime = lines[i + 1].trimmed();
} else if (line.startsWith(QString::fromUtf8("Время:")) && i + 1 < lines.size()) {
bankTime += " " + lines[i + 1].trimmed();
}
}
qDebug() << "[Dushanbe::PayByPhone] Result: date=" << bankTime << "fee=" << fee;
}
// bank_transaction_id: phone аккаунта + unix timestamp (без перехода в историю).
// Уникальность за счёт <bankProfilePhone, секунда>.
const QString operationId = QString("dushanbe_") + bankProfilePhone + "_"
+ QString::number(QDateTime::currentSecsSinceEpoch());
qDebug() << "[Dushanbe::PayByPhone] Generated operationId:" << operationId;
// Сохраняем транзакцию
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.fee = fee;
tx.status = TransactionStatus::Complete;
tx.type = TransactionType::Phone;
tx.phone = m_phone;
tx.bankName = "dushanbe_city_bank";
tx.bankTime = bankTime;
tx.bankTrExternalId = operationId;
tx.timestamp = QDateTime::currentDateTimeUtc();
tx.completeTime = QDateTime::currentDateTimeUtc();
tx.receiptImage = receiptImage;
const int txId = TransactionDAO::insertTransaction(tx);
if (txId != -1) {
qDebug() << "[Dushanbe::PayByPhone] Transaction saved, id:" << txId
<< "operationId:" << operationId;
if (!receiptImage.isEmpty()) {
TransactionDAO::updateReceiptImage(txId, receiptImage);
}
} else {
qWarning() << "[Dushanbe::PayByPhone] Failed to save transaction";
}
}
} // namespace Dushanbe