#include "CommonScript.h" #include #include #include #include #include #include #include "adb/AdbUtils.h" #include "dao/DeviceDAO.h" #include "dao/BankProfileDAO.h" #include "net/NetworkService.h" namespace Dushanbe { CommonScript::CommonScript(QObject *parent) : QObject(parent) { } CommonScript::~CommonScript() = default; void CommonScript::start() { doStart(); emit finished(); } Node CommonScript::swipeToButton( const QString &deviceId, const QString &text, int &counter, const int width, const int height ) { for (int i = 0; i < 10; ++i) { if (Node button = xmlScreenParser.findButtonNode(text, true); button.x() > 0 && button.y() > 0) { return button; } const int x = width / 2; const int offset = height / 4; AdbUtils::makeSwipe(deviceId, x, height - offset, x, offset); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++counter)); } return {}; } bool CommonScript::inputPinCode(const QString &deviceId, const QString &pinCode) { if (pinCode.isEmpty()) { qWarning() << "[Dushanbe::inputPinCode] pinCode is EMPTY — nothing to enter!"; return false; } // Ждём, пока на экране появятся все уникальные цифры из PIN. // DC Next на некоторых устройствах успевает показать заголовок // "Введите код доступа" раньше, чем дорисует кнопочную сетку, — // если начать тапать в этот момент, часть цифр не найдётся и PIN // не введётся целиком. bool seenInPin[10] = {false}; for (const QChar &d : pinCode) { if (!d.isDigit()) continue; const int idx = d.digitValue(); if (idx >= 0 && idx <= 9) seenInPin[idx] = true; } auto allDigitsPresent = [this, &seenInPin]() { for (int d = 0; d < 10; ++d) { if (!seenInPin[d]) continue; if (xmlScreenParser.findNodeByContentDesc(QString::number(d)).isEmpty()) return false; } return true; }; for (int i = 0; i < 6 && !allDigitsPresent(); ++i) { QThread::msleep(500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); } if (!allDigitsPresent()) { qWarning() << "[Dushanbe::inputPinCode] keypad incomplete after wait; aborting"; return false; } qDebug() << "[Dushanbe::inputPinCode] entering" << pinCode.length() << "digits"; // DC Next использует кнопочную сетку для ввода PIN (content-desc = "0"-"9") for (const QChar &digit : pinCode) { Node button = xmlScreenParser.findNodeByContentDesc(QString(digit)); if (button.isEmpty()) { qWarning() << "[Dushanbe::inputPinCode] Button not found for digit:" << digit; return false; } AdbUtils::makeTap(deviceId, button.x(), button.y()); QThread::msleep(300); } return true; } bool CommonScript::runAppAndGoToHomeScreen( const QString &deviceId, const QString &packageName, const QString &pinCode, const int width, const int height ) { AdbUtils::unlockScreen(deviceId, width, height); // Быстрый path: если приложение уже запущено — не перезапускаем сразу, // а смотрим текущий экран. Если это Home — готово. Если PIN — вводим // и ждём Home. Только если не смогли — убиваем и рестартим. if (const QString pid = AdbUtils::tryToGetRunningAppPid(deviceId, packageName); !pid.isEmpty()) { qDebug() << "[Dushanbe] Process already running, pid:" << pid; xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); if (xmlScreenParser.isHomeScreen()) { qDebug() << "[Dushanbe] Fast path: already on Home screen"; return true; } if (xmlScreenParser.isPinCodeScreen()) { qDebug() << "[Dushanbe] Fast path: PIN screen, entering PIN"; if (inputPinCode(deviceId, pinCode)) { QThread::msleep(3000); for (int i = 0; i < 10; ++i) { xmlScreenParser.parseAndSaveXml( AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); if (xmlScreenParser.isHomeScreen()) { qDebug() << "[Dushanbe] Fast path: Home reached after PIN"; return true; } QThread::msleep(1000); } } } qDebug() << "[Dushanbe] Fast path failed, restarting app"; AdbUtils::tryToKillApplication(deviceId, packageName); } if (!AdbUtils::tryToStartApplication(deviceId, packageName)) { AdbUtils::takeScreenshot(deviceId, "run_app_error"); qWarning() << "[Dushanbe] Process has not started"; return false; } QThread::msleep(4500); int counter = 0; bool findAndInputPincode = false; while (true) { counter++; if (counter > 20) { qWarning() << "[Dushanbe] runAppAndGoToHomeScreen: too many attempts, giving up"; AdbUtils::takeScreenshot(deviceId, "too_many_attempts"); return false; } xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); if (xmlScreenParser.isHomeScreen()) { qDebug() << "[Dushanbe] Home reached on attempt" << counter; return true; } if (!findAndInputPincode && xmlScreenParser.isPinCodeScreen()) { qDebug() << "[Dushanbe] PIN screen detected on attempt" << counter << "entering PIN"; if (inputPinCode(deviceId, pinCode)) { findAndInputPincode = true; } else { // Клавиатура могла не дорисоваться или PIN ввёлся частично — // оставляем флаг false, следующая итерация повторит ввод, // когда кнопки появятся. qWarning() << "[Dushanbe] inputPinCode failed on attempt" << counter << "— will retry"; } QThread::msleep(3000); continue; } // Диагностика: что реально на экране? QStringList sample; for (const Node &n : xmlScreenParser.findAllNodes()) { if (n.contentDesc.isEmpty() && n.text.isEmpty()) continue; QString s = !n.contentDesc.isEmpty() ? n.contentDesc : n.text; s.replace('\n', ' '); if (s.size() > 60) s = s.left(60) + "..."; sample.append(s); if (sample.size() >= 8) break; } qDebug() << "[Dushanbe] attempt" << counter << "not home, not PIN. Sample:" << sample; QThread::msleep(1000); } } void CommonScript::postAccountsData(const QList> &accounts) { if (accounts.isEmpty()) { return; } auto *thread = new QThread; auto *worker = new NetworkService; QJsonArray list; for (const QPair &pair : accounts) { QJsonObject obj; MaterialInfo account = pair.first; obj["appName"] = account.appCode; obj["lastNumbers"] = account.lastNumbers; obj["amount"] = account.amount; obj["description"] = account.description; obj["currency"] = account.currency; obj["status"] = materialStatusToString(account.status); list.append(obj); } worker->setDeviceId(accounts.first().first.deviceId); worker->setPayload(QJsonDocument(list).toJson()); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &NetworkService::postAccountsData); connect(worker, &NetworkService::finished, thread, &QThread::quit); connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } void CommonScript::postNewTransactionData(const MaterialInfo &account, const TransactionInfo &transaction) { auto *thread = new QThread; auto *worker = new NetworkService; QJsonObject obj; obj["id"] = transaction.id; obj["amount"] = transaction.amount; obj["name"] = transaction.name; obj["fee"] = transaction.fee; obj["status"] = transactionStatusToString(transaction.status); obj["type"] = transactionTypeToString(transaction.type); obj["phone"] = transaction.phone; obj["description"] = transaction.description; obj["updatedDescription"] = transaction.updatedDescription; obj["bankName"] = transaction.bankName; obj["bankTime"] = transaction.bankTime; obj["bankTransactionId"] = transaction.bankTrExternalId; worker->setDeviceId(account.deviceId); worker->addExtra("lastNumbers", account.lastNumbers); worker->addExtra("transactionId", transaction.id); worker->setPayload(QJsonDocument(obj).toJson()); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &NetworkService::insertTransactionData); connect(worker, &NetworkService::finished, thread, &QThread::quit); connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } void CommonScript::updateTransactionData( const MaterialInfo &account, const int transactionId, const TransactionStatus status, const QString &oldDesc, const QString &newDesc ) { auto *thread = new QThread; auto *worker = new NetworkService; QJsonObject obj; obj["id"] = transactionId; obj["status"] = transactionStatusToString(status); obj["description"] = newDesc; obj["updatedDescription"] = oldDesc; worker->setDeviceId(account.deviceId); worker->addExtra("lastNumbers", account.lastNumbers); worker->setPayload(QJsonDocument(obj).toJson()); worker->moveToThread(thread); connect(thread, &QThread::started, worker, &NetworkService::updateTransactionData); connect(worker, &NetworkService::finished, thread, &QThread::quit); connect(worker, &NetworkService::finished, worker, &QObject::deleteLater); connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); } QString CommonScript::createBankProfileIfNeeded(const QString &deviceId, const QString &appCode, double parsedBalance) { const BankProfileInfo app = BankProfileDAO::getApplication(deviceId, appCode); if (app.fullName.trimmed().isEmpty() || app.phone.trimmed().isEmpty()) { qWarning() << "[Dushanbe] createBankProfileIfNeeded: skipping — fullName or phone is empty"; return {}; } const QMap currencyCodes = { {"RUB", 643}, {"USD", 840}, {"KRW", 410}, {"AZN", 944}, {"ARS", 32}, {"TJS", 972} }; const int currencyCode = currencyCodes.value(app.currency.toUpper(), 840); const QStringList parts = app.fullName.split(' ', Qt::SkipEmptyParts); const QSettings settings("config.ini", QSettings::IniFormat); const QString phoneCode = settings.value(appCode + "/phone_code").toString(); QString phone = app.phone; phone.remove(QRegularExpression("[^0-9]")); if (!phoneCode.isEmpty() && !phone.startsWith(phoneCode)) { if (phone.startsWith('8') && phoneCode != "8") { phone.replace(0, 1, phoneCode); } else { phone.prepend(phoneCode); } } phone.prepend('+'); const DeviceInfo device = DeviceDAO::getDeviceById(deviceId); QJsonObject profileBody; profileBody["first_name"] = parts.value(0); profileBody["middle_name"] = parts.size() >= 3 ? parts.value(1) : QString(""); profileBody["last_name"] = parts.size() >= 3 ? parts.value(2) : parts.value(1); profileBody["bank_name"] = appCode; profileBody["phone_number"] = phone; const double balance = (parsedBalance >= 0.0) ? parsedBalance : app.amount; profileBody["balance"] = QString::number(qRound64(balance * 100)); profileBody["state"] = (app.status == "active") ? "enabled" : "disabled"; profileBody["device_id"] = device.apiId; profileBody["currency"] = currencyCode; NetworkService ns; ns.setDeviceId(deviceId); ns.setPayload(QJsonDocument(profileBody).toJson()); if (app.bankProfileId.isEmpty()) { ns.addExtra("app_id", app.id); ns.postBankProfile(); const BankProfileInfo afterPost = BankProfileDAO::getApplication(deviceId, appCode); if (afterPost.bankProfileId.isEmpty()) { QString err; if (ns.lastResponseBody().contains("already exists", Qt::CaseInsensitive)) { err = "PROFILE_ALREADY_EXISTS: " + ns.lastServerMessage(); } else { err = "postBankProfile failed for " + deviceId + " " + appCode; } qWarning() << "[Dushanbe] createBankProfileIfNeeded:" << err; return err; } qDebug() << "[Dushanbe] createBankProfileIfNeeded: postBankProfile done"; } else { ns.addExtra("bank_profile_id", app.bankProfileId); ns.patchBankProfile(); qDebug() << "[Dushanbe] createBankProfileIfNeeded: patchBankProfile done"; } return {}; } void CommonScript::stop() { m_running = false; } bool CommonScript::closeGooglePlayBannerIfVisible( const QString &deviceId, const int width, const int height ) { // 1. Системный диалог запроса разрешения Android (permissioncontroller) — тапаем "ЗАПРЕТИТЬ" if (Node denyBtn = xmlScreenParser.tryToFindSystemPermissionDenyButton(); !denyBtn.isEmpty()) { qDebug() << "[Dushanbe] System permission dialog detected mid-task, tapping 'ЗАПРЕТИТЬ'..."; AdbUtils::makeTap(deviceId, denyBtn.x(), denyBtn.y()); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); return true; } // 2. Google Play in-app update bottom sheet Node gpNode = xmlScreenParser.tryToFindGooglePlayBanner(width, height); if (gpNode.isEmpty()) return false; qDebug() << "[Dushanbe] Google Play update banner detected mid-task, closing..."; AdbUtils::makeTap(deviceId, gpNode.x(), gpNode.y()); QThread::msleep(1500); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); return true; } } // namespace Dushanbe