1
0
forked from BRT/arc
arc/android/ozon/PayByPhoneScript.cpp
slava af47dc5888 Improve "Продолжить" button handling in PayByPhoneScript:
- Add keyboard dismissal and retry logic to ensure button visibility and tappability.
2026-04-24 22:07:34 +07:00

587 lines
26 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 <QProcess>
#include <QRegularExpression>
#include <QThread>
#include <QTimeZone>
#include <utility>
#include <tesseract/baseapi.h>
#include <leptonica/allheaders.h>
#include "adb/AdbUtils.h"
#include "dao/MaterialDAO.h"
#include "dao/BankProfileDAO.h"
#include "dao/DeviceDAO.h"
#include "dao/TransactionDAO.h"
#include "time/DateUtils.h"
#include <QCoreApplication>
static QString recognizeTextFromImage(const QByteArray &pngData) {
auto *api = new tesseract::TessBaseAPI();
if (api->Init(QCoreApplication::applicationDirPath().toUtf8().constData(), "rus+eng")) {
qWarning() << "[OCR] Failed to init Tesseract";
delete api;
return {};
}
Pix *pix = pixReadMem(reinterpret_cast<const l_uint8 *>(pngData.constData()),
static_cast<size_t>(pngData.size()));
if (!pix) {
qWarning() << "[OCR] Failed to read image";
api->End();
delete api;
return {};
}
api->SetImage(pix);
char *text = api->GetUTF8Text();
QString result = QString::fromUtf8(text).trimmed();
delete[] text;
pixDestroy(&pix);
api->End();
delete api;
return result;
}
static QString extractTransactionId(const QString &ocrText) {
// "ID операции B606513042211200000120011700501"
// "ID операции po-3140eb01-002c-5000-8000-033396b637e1"
// или разбит на 2 строки
QRegularExpression reId(R"(ID\s+операции\s*=?\s*([A-Za-zА-Яа-яЁё0-9\-]+(?:[^\S\n]+[A-Za-zА-Яа-яЁё0-9\-]+)?))");
if (auto m = reId.match(ocrText); m.hasMatch()) {
QString id = m.captured(1);
id.remove(' ');
id.remove(QChar(0x00A0));
return id;
}
// "№ Ф-2026-25235663"
QRegularExpression reRef(R"(№\s*(Ф-[\d-]+))");
if (auto m = reRef.match(ocrText); m.hasMatch()) return m.captured(1);
return {};
}
namespace Ozon {
PayByPhoneScript::PayByPhoneScript(
MaterialInfo account,
QString phone,
QString bankName,
const double amount,
QObject *parent
) : CommonScript(parent),
m_account(std::move(account)), m_phone(std::move(phone)),
m_bankName(std::move(bankName)), 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 QString &adbId = device.adbSerial;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
// Закрываем рекламный bottom sheet
if (Node adNode = xmlScreenParser.tryToFindAdBanner(); !adNode.isEmpty()) {
qDebug() << "[PayByPhone] Ad banner detected, closing...";
AdbUtils::makeTap(adbId, adNode.x(), adNode.y());
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Если не на домашнем экране — перезапускаем
if (!xmlScreenParser.isHomeScreen()) {
qDebug() << "[PayByPhone] Not on home screen, restarting app...";
if (!runAppAndGoToHomeScreen(adbId, app.package, app.pinCode,
device.screenWidth, device.screenHeight)) {
m_error = "Cannot reach home screen: " + device.name + " (" + device.id + ")";
qWarning() << m_error;
AdbUtils::tryToKillApplication(adbId, app.package);
emit finishedWithResult(m_error);
return;
}
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Закрываем баннеры
tryToCloseAllBannersOnHomePage(adbId, device.screenWidth, device.screenHeight);
QThread::msleep(500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
// Закрываем баннер Google Play "Доступно обновление"
if (Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(device.screenWidth, device.screenHeight); !gpNode.isEmpty()) {
qDebug() << "[PayByPhone] Google Play update banner detected, closing...";
AdbUtils::makeTap(adbId, gpNode.x(), gpNode.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
// Закрываем баннер "Заказать карту" — тапаем сверху экрана
if (Node orderCardNode = xmlScreenParser.findTextNode("Заказать карту"); !orderCardNode.isEmpty()) {
qDebug() << "[PayByPhone] 'Заказать карту' banner detected, closing...";
AdbUtils::makeTap(adbId, device.screenWidth / 2, device.screenHeight / 20);
QThread::msleep(1000);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(adbId, ++m_counter));
}
makePayment(device.adbSerial, app.package, app.pinCode, app.phone, device.screenWidth, device.screenHeight);
emit finishedWithResult(m_error);
}
void PayByPhoneScript::makePayment(
const QString &deviceId,
const QString &packageName,
const QString &pinCode,
const QString &bankProfilePhone,
const int width,
const int height
) {
// Макрос-проверка: приложение живо?
auto checkApp = [&]() -> bool {
if (!isAppRunning(deviceId, packageName)) {
m_error = "Приложение было закрыто";
qWarning() << "[PayByPhone]" << m_error;
return false;
}
return true;
};
// 1. Нажимаем "Перевести" на домашнем экране
Node transferBtn = xmlScreenParser.findButtonNode("Перевести", false);
if (transferBtn.isEmpty()) {
m_error = "Кнопка 'Перевести' не найдена на домашнем экране";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Tapping 'Перевести'...";
AdbUtils::makeTap(deviceId, transferBtn.x(), transferBtn.y());
QThread::msleep(2000);
// 2. Ждём экран "Платежи и переводы" с полем "Телефон получателя"
bool paymentsPageLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
Node phoneField = xmlScreenParser.findFirstEditText();
if (!phoneField.isEmpty()) {
paymentsPageLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for payments page, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!paymentsPageLoaded) {
m_error = "Экран 'Платежи и переводы' не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 3. Тапаем на поле "Телефон получателя" чтобы перейти на экран "Кому перевести"
Node phoneFieldTap = xmlScreenParser.findFirstEditText();
qDebug() << "[PayByPhone] Tapping 'Телефон получателя'...";
AdbUtils::makeTap(deviceId, phoneFieldTap.x(), phoneFieldTap.y());
QThread::msleep(2000);
// 4. Ждём экран "Кому перевести"
bool phoneScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
// Закрываем диалог "Разрешить доступ к контактам"
Node notNowBtn = xmlScreenParser.findButtonNode("Не сейчас", false);
if (!notNowBtn.isEmpty()) {
qDebug() << "[PayByPhone] Contacts permission dialog detected, tapping 'Не сейчас'...";
AdbUtils::makeTap(deviceId, notNowBtn.x(), notNowBtn.y());
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
}
Node title = xmlScreenParser.findTextNode("Кому перевести");
if (!title.isEmpty()) {
phoneScreenLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Кому перевести', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!phoneScreenLoaded) {
m_error = "Экран 'Кому перевести' не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 5. Тапаем на EditText и вводим номер телефона
Node phoneInput;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
phoneInput = xmlScreenParser.findFirstEditText();
if (!phoneInput.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for phone input field, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (phoneInput.isEmpty()) {
m_error = "Поле ввода телефона не найдено";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, phoneInput.x(), phoneInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, m_phone);
QThread::msleep(2000);
// 6. Ждём экран "По номеру телефона", обрабатывая промежуточные экраны
bool sumScreenLoaded = false;
bool bankSelected = false;
bool otherBankClicked = false;
for (int attempt = 0; attempt < 20; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
// Проверяем: уже на экране суммы?
Node sumTitle = xmlScreenParser.findTextNode("По номеру телефона");
if (!sumTitle.isEmpty() && sumTitle.y() < height / 7) {
sumScreenLoaded = true;
break;
}
// Ищем кнопку с номером телефона (новый перевод / последние переводы)
QString phoneDigits = m_phone;
phoneDigits.remove(QRegularExpression("[^0-9]"));
bool foundPhoneBtn = false;
for (const Node &btn : xmlScreenParser.findAllButtons()) {
QString btnDigits = btn.text;
btnDigits.remove(QRegularExpression("[^0-9]"));
if (btnDigits.contains(phoneDigits) || phoneDigits.contains(btnDigits.right(10))) {
qDebug() << "[PayByPhone] Found button with phone number:" << btn.text;
AdbUtils::makeTap(deviceId, btn.x(), btn.y());
QThread::msleep(2000);
foundPhoneBtn = true;
break;
}
}
if (foundPhoneBtn) continue;
// Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка"
if (!bankSelected) {
// Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой)
bool foundOtherBank = false;
{
Node node;
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (n.contentDesc.contains("other-bank-item")) {
node = n;
break;
}
}
if (!node.isEmpty()) {
qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item)...";
AdbUtils::makeTap(deviceId, node.x(), node.y());
QThread::msleep(2000);
foundOtherBank = true;
otherBankClicked = true;
}
}
if (foundOtherBank) continue;
// Вариант 2: уже на экране поиска банка (EditText) — только после клика на "Другой банк"
if (otherBankClicked) {
Node bankInput = xmlScreenParser.findFirstEditText();
if (!bankInput.isEmpty()) {
qDebug() << "[PayByPhone] Bank search screen detected, entering bank name...";
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
// Ввод кириллицы через ADB Keyboard
if (!AdbUtils::enableAdbIMEKeyboard(deviceId)) {
m_error = "Не удалось активировать ADB Keyboard (см. логи 'ime list -s' / default_input_method)";
qWarning() << "[PayByPhone]" << m_error;
return;
}
QThread::msleep(500);
AdbUtils::makeTap(deviceId, bankInput.x(), bankInput.y());
QThread::msleep(500);
AdbUtils::inputAdbIMEText(deviceId, m_bankName);
QThread::msleep(2000);
AdbUtils::disableAdbIMEKeyboard(deviceId);
qDebug() << "[PayByPhone] Entered bank name via ADB IME:" << m_bankName;
QThread::msleep(2000);
// Ищем банк в списке и кликаем по нему (кнопка с числовым content-desc)
bool bankFound = false;
for (int ba = 0; ba < 10; ++ba) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
for (const Node &node : xmlScreenParser.findAllNodes()) {
if (node.className == "android.widget.Button"
&& !node.contentDesc.isEmpty()
&& node.contentDesc != "base-go-back-button"
&& node.contentDesc != "native-toolbar-close-button"
&& node.contentDesc != "clear-input-icon") {
const int tapX = node.x1() + node.width() / 4;
qDebug() << "[PayByPhone] Found bank in list, tapping:" << node.contentDesc << "tapX:" << tapX << "y:" << node.y();
AdbUtils::makeTap(deviceId, tapX, node.y());
bankFound = true;
break;
}
}
if (bankFound) break;
qDebug() << "[PayByPhone] Waiting for bank list, attempt" << ba + 1 << "/ 10";
QThread::msleep(1500);
}
if (!bankFound) {
m_error = "Банк '" + m_bankName + "' не найден в списке";
qWarning() << "[PayByPhone]" << m_error;
return;
}
bankSelected = true;
QThread::msleep(2000);
continue;
}
} // otherBankClicked
}
qDebug() << "[PayByPhone] Waiting for 'По номеру телефона', attempt" << attempt + 1 << "/ 20";
QThread::msleep(1500);
}
if (!sumScreenLoaded) {
m_error = "Экран ввода суммы не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 7. Вводим сумму
Node sumInput;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
sumInput = xmlScreenParser.findFirstEditText();
if (!sumInput.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for sum input field, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
}
if (sumInput.isEmpty()) {
m_error = "Поле ввода суммы не найдено";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, sumInput.x(), sumInput.y());
QThread::msleep(500);
AdbUtils::inputText(deviceId, QString::number(static_cast<int>(m_amount)));
QThread::msleep(1000);
// Закрываем системную софт-клавиатуру, иначе она перекрывает "Продолжить"
// (кнопка уезжает за нижнюю границу web_view_holder и имеет нулевую высоту).
AdbUtils::hideKeyboard(deviceId);
QThread::msleep(800);
// 6. Нажимаем "Продолжить" (ретраим, пока bounds перестанут быть нулевыми)
Node continueBtn;
for (int attempt = 0; attempt < 5; ++attempt) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
continueBtn = xmlScreenParser.findButtonNode("Продолжить", false);
if (!continueBtn.isEmpty() && continueBtn.height() > 0) break;
qDebug() << "[PayByPhone] Waiting for tappable 'Продолжить', attempt" << attempt + 1 << "/ 5"
<< "(bounds collapsed — клавиатура ещё видна?)";
AdbUtils::hideKeyboard(deviceId);
QThread::msleep(1000);
}
if (continueBtn.isEmpty() || continueBtn.height() == 0) {
m_error = "Кнопка 'Продолжить' не найдена или перекрыта клавиатурой";
qWarning() << "[PayByPhone]" << m_error;
return;
}
AdbUtils::makeTap(deviceId, continueBtn.x(), continueBtn.y());
QThread::msleep(2000);
// 7. Ждём экран "Подтвердите перевод" с кнопкой "Перевести ..."
bool confirmScreenLoaded = false;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
Node confirmTitle = xmlScreenParser.findTextNode("Подтвердите перевод");
if (!confirmTitle.isEmpty()) {
confirmScreenLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Подтвердите перевод', attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
}
if (!confirmScreenLoaded) {
m_error = "Экран подтверждения не загрузился";
qWarning() << "[PayByPhone]" << m_error;
return;
}
// 8. Нажимаем "Перевести ..." (ищем по первому слову)
Node transferConfirmBtn;
for (int attempt = 0; attempt < 10; ++attempt) {
if (!checkApp()) return;
transferConfirmBtn = xmlScreenParser.findButtonNode("Перевести", true);
if (!transferConfirmBtn.isEmpty()) break;
qDebug() << "[PayByPhone] Waiting for 'Перевести ...' button, attempt" << attempt + 1 << "/ 10";
QThread::msleep(1500);
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
}
if (transferConfirmBtn.isEmpty()) {
m_error = "Кнопка 'Перевести ...' не найдена на экране подтверждения";
qWarning() << "[PayByPhone]" << m_error;
return;
}
qDebug() << "[PayByPhone] Tapping confirm button:" << transferConfirmBtn.text;
AdbUtils::makeTap(deviceId, transferConfirmBtn.x(), transferConfirmBtn.y());
QThread::msleep(3000);
// 8.5. Проверяем экран ввода PIN-кода
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByPhone] PIN code screen detected after confirm, entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
}
// 9. Ждём результат: "Успешно" или "Не удалось"
for (int attempt = 0; attempt < 15; ++attempt) {
if (!checkApp()) return;
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(deviceId, width, height);
// Если PIN-код появился позже — вводим
if (const QList<Node> pinNodes = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
!pinNodes.isEmpty()) {
qDebug() << "[PayByPhone] PIN code screen detected (retry), entering PIN...";
inputPinCode(deviceId, pinNodes);
QThread::msleep(3000);
continue;
}
if (!xmlScreenParser.findTextNode("Успешно").isEmpty()) {
qDebug() << "[PayByPhone] Payment successful!";
m_error.clear();
// Сохраняем транзакцию в БД
TransactionInfo tx;
tx.accountId = m_account.id;
tx.amount = -m_amount;
tx.phone = m_phone;
tx.bankName = "ozon";
tx.type = TransactionType::Phone;
tx.status = TransactionStatus::Complete;
tx.timestamp = DateUtils::getUtcNow();
tx.completeTime = QDateTime::currentDateTime().toTimeZone(QTimeZone("Europe/Moscow"));
tx.bankTime = tx.completeTime.toString("dd.MM.yyyy, HH:mm");
const int txId = TransactionDAO::insertTransaction(tx);
qDebug() << "[PayByPhone] Saved transaction to DB, id:" << txId;
// Ищем кнопку "Документы" и достаём ID операции
QString externalId;
Node docBtn = xmlScreenParser.findButtonNode("Документы", false);
if (!docBtn.isEmpty() && txId != -1) {
qDebug() << "[PayByPhone] Tapping 'Документы'...";
AdbUtils::makeTap(deviceId, docBtn.x(), docBtn.y());
QThread::msleep(2000);
// Ждём появления кнопки "Поделиться"
bool docLoaded = false;
for (int da = 0; da < 20; ++da) {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter));
Node shareBtn = xmlScreenParser.findNodeByContentDesc("Поделиться");
if (!shareBtn.isEmpty()) {
docLoaded = true;
break;
}
qDebug() << "[PayByPhone] Waiting for 'Поделиться', attempt" << da + 1 << "/ 20";
QThread::msleep(1500);
}
if (docLoaded) {
QThread::msleep(500);
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(deviceId);
if (!screenshot.isEmpty()) {
m_resultScreenshot = screenshot;
emit screenshotReady(m_resultScreenshot);
TransactionDAO::updateReceiptImage(txId, screenshot);
qDebug() << "[PayByPhone] Receipt saved to DB, txId:" << txId;
const QString ocrText = recognizeTextFromImage(screenshot);
if (!ocrText.isEmpty()) {
qDebug() << "[PayByPhone] OCR result:\n" << ocrText;
externalId = extractTransactionId(ocrText);
}
}
} else {
qWarning() << "[PayByPhone] 'Поделиться' not found, skipping receipt";
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
}
if (externalId.isEmpty() && txId != -1) {
externalId = "ozon_" + bankProfilePhone + "_" + QString::number(QDateTime::currentSecsSinceEpoch());
qDebug() << "[PayByPhone] Generated fallback transaction ID:" << externalId;
}
if (!externalId.isEmpty() && txId != -1) {
TransactionDAO::updateBankTrExternalId(txId, externalId);
qDebug() << "[PayByPhone] Transaction ID:" << externalId;
}
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
return;
}
if (!xmlScreenParser.findTextNode("Не удалось").isEmpty()) {
m_error = "Перевод не удался (экран 'Не удалось')";
qWarning() << "[PayByPhone]" << m_error;
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
AdbUtils::goBack(deviceId);
QThread::msleep(1000);
return;
}
qDebug() << "[PayByPhone] Waiting for payment result, attempt" << attempt + 1 << "/ 15";
QThread::msleep(2000);
}
m_error = "Не дождались результата перевода";
qWarning() << "[PayByPhone]" << m_error;
}
} // namespace Ozon