diff --git a/automation_script/AdbUtils.cpp b/automation_script/AdbUtils.cpp new file mode 100644 index 0000000..edd71c5 --- /dev/null +++ b/automation_script/AdbUtils.cpp @@ -0,0 +1,84 @@ +#include "AdbUtils.h" +#include +#include +#include +#include + +AdbUtils::AdbUtils(QObject *parent) : QObject(parent) { +} + +bool AdbUtils::checkDevices() { + QProcess process; + process.start("adb devices"); + process.waitForFinished(); + QString output = process.readAllStandardOutput(); + return output.contains("device"); +} + +bool AdbUtils::startApp(const QString &packageName) { + QProcess process; + QString command = QString("adb shell monkey -p %1 -c android.intent.category.LAUNCHER 1").arg(packageName); + process.start(command); + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +bool AdbUtils::dumpAndPullUiXml(const QString &localPath) { + QProcess process; + process.start("adb shell uiautomator dump /sdcard/view.xml"); + process.waitForFinished(); + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + qDebug() << "Ошибка в получении дампа UI на устройстве"; + return false; + } + + QString pullCmd = QString("adb pull /sdcard/view.xml %1").arg(localPath); + process.start(pullCmd); + process.waitForFinished(); + if (process.exitStatus() != QProcess::NormalExit || process.exitCode() != 0) { + qDebug() << "Ошибка в передаче xml файла на компьютер"; + return false; + } + + return true; +} + +bool AdbUtils::tap(int x, int y) { + QProcess process; + QString cmd = QString("adb shell input tap %1 %2").arg(x).arg(y); + process.start(cmd); + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +bool AdbUtils::swipe(const int x1, const int y1, const int x2, const int y2) { + QProcess process; + QString cmd = QString("adb shell input swipe %1 %2 %3 %4").arg(x1).arg(y1).arg(x2).arg(y2); + process.start(cmd); + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +bool AdbUtils::inputText(const QString &text) { + QProcess process; + QString cmd = QString("adb shell input text \"%1\"").arg(text); + process.start(cmd); + process.waitForFinished(); + return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +} + +// bool AdbUtils::takeScreenshot(const QString& filename) { +// QProcess process; +// QDir().mkdir("screens"); // Создаем папку, если её нет +// QString cmd = QString("adb exec-out screencap -p > screens/%1").arg(filename); +// process.start(cmd); +// process.waitForFinished(); +// return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0; +// } + +// void AdbUtils::log(const std::string& message) { +// std::ofstream out("log.txt", std::ios::app); +// std::time_t t = std::time(nullptr); +// out << "[" << std::put_time(std::localtime(&t), "%H:%M:%S") << "] " << message << std::endl; +// std::cout << message << std::endl; +// } diff --git a/automation_script/AdbUtils.h b/automation_script/AdbUtils.h new file mode 100644 index 0000000..20a3697 --- /dev/null +++ b/automation_script/AdbUtils.h @@ -0,0 +1,29 @@ +#ifndef ADBUTILS_H +#define ADBUTILS_H + +#include + +class AdbUtils : public QObject { + Q_OBJECT + +public: + explicit AdbUtils(QObject *parent = nullptr); + + static bool checkDevices(); + + static bool startApp(const QString &packageName); + + static bool tap(int x, int y); + + static bool inputText(const QString &text); + + static bool swipe(int x1, int y1, int x2, int y2); + + static bool dumpAndPullUiXml(const QString &localPath); + + // static bool takeScreenshot(const QString& filename); + + // void log(const QString& message); +}; + +#endif // ADBUTILS_H diff --git a/automation_script/Script.cpp b/automation_script/Script.cpp new file mode 100644 index 0000000..acddeb7 --- /dev/null +++ b/automation_script/Script.cpp @@ -0,0 +1,483 @@ +// +// Created by AmirS on 24.04.2025. +// + +#include "Script.h" +#include "AdbUtils.h" +#include "XmlParser.h" +#include "QDebug" +#include +#include +#include +#include + +// Вспомогательные функции + +void wait(const int milliseconds) { + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); +} + +Script::Script(QObject *parent) : QObject(parent) { +} + +Script::~Script() = default; + +void Script::start() { + qDebug("Запуск скрипта..."); + + processUiLoop(); +} + +void Script::processUiLoop() { + while (true) { + QString uiDumpPath = "ui_dump.xml"; + AdbUtils::dumpAndPullUiXml(uiDumpPath); + + QList elements = XmlParser::parseUiDumpUsingQXml(uiDumpPath); + + switch (currentState) { + case State::WaitingForPinInput: + if (XmlParser::isPinCodeScreen(elements)) { + qDebug() << "Экран ввода ПИН-кода обнаружен"; + inputPinCode("1211"); + currentState = State::ClosingBanner; + } + break; + + case State::ClosingBanner: + handleClosingBanner(elements); + break; + + case State::OnMainScreen: + if (XmlParser::isMainScreen(elements)) { + qDebug() << "State: OnMainScreen -> Поиск кнопки 'Все продукты'"; + swipeToFindElement("Все продукты"); + currentState = State::SearchingCard; + } + break; + + case State::SearchingCard: + qDebug() << "State: SearchingCard -> Поиск блока карты"; + // Добавить сюда поиск карты по последним 4 цифрам, известным для выбора карты + currentState = State::OnCardDetails; + break; + + case State::OnCardDetails: + // Обработка экрана перевода + handleTransferScreen(elements); + break; + + case State::OnTransferScreen: + qDebug() << "State: OnTransferScreen -> Перевод выполнен успешно."; + currentState = State::Idle; + break; + + case State::Idle: + default: + qDebug() << "State: Idle -> Waiting for action"; + if (XmlParser::isPinCodeScreen(elements)) { + currentState = State::WaitingForPinInput; + } + break; + } + + QThread::sleep(1); // Пауза между проверками экрана + } +} + + +// Функция для запуска скрипта с передачей параметров +// void Script::runScript(const QString& pin, const QString& last4digits) { +// qDebug("Запуск скрипта для ru.rshb.dbo"); +// +// if (!AdbUtils::checkDevices()) { +// qDebug("ADB: Нет устройств"); +// return; +// } +// +// if (!AdbUtils::startApp("ru.rshb.dbo")) { +// qDebug("Не удалось запустить приложение"); +// return; +// } +// +// wait(4500); +// AdbUtils::dumpAndPullUiXml("ui_01.xml"); +// +// auto ui = XmlParser::parseUiDumpUsingQXml("ui_01.xml"); +// +// if (XmlParser::isPinCodeScreen(ui)) { +// qDebug("Экран PIN-кода"); +// +// // Находим координаты цифр на экране и вводим ПИН +// std::vector pinButtons = getPinButtons(ui); +// for (size_t i = 0; i < pin.length(); ++i) { +// char digit = pin[i]; +// for (const auto& el : pinButtons) { +// if (el.text == std::string(1, digit)) { +// AdbUtils::tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); +// break; +// } +// } +// } +// wait(1000); +// } +// +// // Появляющиеся после входа по ПИН баннеры — цикл для обработки разных состояний показов модальных окон +// bool postLoginScreenFound = false; +// int attempts = 5; // Максимальное количество попыток парсинга xml +// while (attempts-- > 0) { +// AdbUtils::dumpAndPullUiXml("ui_02.xml"); +// auto ui2 = XmlParser::parseUiDumpUsingQXml("ui_02.xml"); +// +// // Закрытие баннеров или диалогов +// if (handlePostLoginScreen(ui2)) { +// postLoginScreenFound = true; +// break; +// } +// +// // Проверка, если мы на главном экране +// if (isMainScreen(ui2)) { +// qDebug("Главный экран подтверждён"); +// postLoginScreenFound = true; +// break; +// } +// +// // Ждем немного перед следующим запросом дампа экрана +// wait(1000); +// } +// +// if (!postLoginScreenFound) { +// qDebug("Не удалось определить пост-логин баннер."); +// return; +// } +// +// // Пролистывание до кнопки "Все продукты" +// AdbUtils::swipe(1000, 2000, 1000, 1300); +// wait(500); +// +// // Нажимаем на кнопку "Все продукты" +// AdbUtils::dumpAndPullUiXml("ui_03.xml"); +// auto ui3 = XmlParser::parseUiDumpUsingQXml("ui_03.xml"); +// +// // Ищем кнопку "Все продукты" и нажимаем на неё +// tapByText(ui3, "Все продукты"); +// wait(800); +// +// // Шаг 6: Выбор карты на экране "Все продукты" +// // Запрашиваем дамп UI для поиска карт +// AdbUtils::dumpAndPullUiXml("ui_04.xml"); +// auto ui4 = XmlParser::parseUiDumpUsingQXml("ui_04.xml"); +// +// // Определим, какую карту выбрать, например, с 4 цифрами "0823" +// UiElement cardButton = findCardByLast4Digits(ui4, last4digits); +// if (cardButton.text.isEmpty()) { +// qDebug("Не найдена карта с номером, содержащим " + last4digits); +// return; +// } +// +// // Нажимаем на карту +// AdbUtils::tap((cardButton.x1 + cardButton.x2) / 2, (cardButton.y1 + cardButton.y2) / 2); +// wait(1000); +// +// // Нажимаем на кнопку "Оплатить" +// // Запрашиваем дамп UI для поиска кнопки "Оплатить" +// AdbUtils::dumpAndPullUiXml("ui_05.xml"); +// auto ui5 = XmlParser::parseUiDumpUsingQXml("ui_05.xml"); +// +// // Ищем кнопку "Оплатить" и нажимаем на неё +// UiElement payBtn = findPayButton(ui5); +// if (payBtn.text.isEmpty()) { +// qDebug("Не найдена кнопка 'Оплатить'"); +// return; +// } +// +// // Нажимаем на кнопку "Оплатить" +// AdbUtils::tap((payBtn.x1 + payBtn.x2) / 2, (payBtn.y1 + payBtn.y2) / 2); +// qDebug("Нажата кнопка 'Оплатить'"); +// wait(1000); +// } + +// Функции для определения экранов и действий + +void Script::inputPinCode(const QString &pinCode) { + // Перебираем каждый символ из ПИН-кода + for (int i = 0; i < pinCode.length(); ++i) { + QString digit = pinCode.mid(i, 1); + qDebug() << "Finding button for digit: " << digit; + + // Поиск кнопки с соответствующим текстом + QList elements = XmlParser::parseUiDumpUsingQXml("ui_dump.xml"); + + // Ищем кнопку с цифрой + bool found = false; + for (const UiElement &el: elements) { + if (el.text() == digit && el.isClickable()) { + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по кнопке + found = true; + break; + } + } + + // Если не нашли кнопку для цифры, выводим предупреждение + if (!found) { + qWarning() << "Digit button for " << digit << " not found!"; + } + + // Пауза между нажатиями, чтобы избежать слишком быстрого ввода + QThread::sleep(1); + } +} + +void Script::handleClosingBanner(QList &elements) { + while (true) { + // Проверка на наличие баннера или диалога + if (XmlParser::isBannerOrDialog(elements)) { + qDebug() << "State: ClosingBanner -> Found banner/dialog. Closing it."; + + // Найдем кнопку "Закрыть" или кнопку без текста в правом верхнем углу + bool closed = false; + + for (const UiElement &el: elements) { + if (el.isClickable()) { + // Флаг для отслеживания, если мы нашли хотя бы одно окно для закрытия + bool bannerClosed = false; + // Пробуем закрыть баннер, если есть кнопка "Закрыть" + if ((el.text() == "Закрыть" || el.text() == "Позже") && el.className() == "android.widget.Button") { + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по кнопке "Закрыть" + closed = true; + bannerClosed = true; + qDebug() << "Banner/dialog closed!"; + break; + } + // Если кнопка без текста и в правой верхней части + if (el.text().isEmpty() && el.className() == "android.widget.Button" && + el.x1() > 900 && el.y1() < 100) { + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по кнопке в правом верхнем углу + closed = true; + bannerClosed = true; + qDebug() << "Banner/dialog closed by tapping button in the top right corner."; + break; + } + } + } + + if (!closed) { + qWarning() << "Невозможно закрыть баннер или диалог!"; + break; // Прерываем цикл, если не удалось закрыть окно + } + + // Пауза перед следующим закрытием окна + QThread::sleep(1); + } else { + qDebug() << "State: ClosingBanner -> No banners/dialogs left to close."; + break; // Заканчиваем цикл, если баннеров/диалогов больше нет + } + } + + // Переход к следующему состоянию + currentState = State::OnMainScreen; // Если все модальные окна закрыты, переходим к основному экрану +} + +void Script::inputAmount(const QString &amount) { + // Разделяем сумму на отдельные цифры + for (int i = 0; i < amount.length(); ++i) { + QString digit = amount.mid(i, 1); + qDebug() << "Inputting digit: " << digit; + + // Поиск кнопки с соответствующей цифрой + QList elements = XmlParser::parseUiDumpUsingQXml("ui_dump.xml"); + + bool found = false; + for (const UiElement &el: elements) { + if (el.text() == digit && el.isClickable()) { + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по кнопке цифры + found = true; + break; + } + } + + if (!found) { + qWarning() << "Digit button for " << digit << " not found!"; + } + + QThread::sleep(1); // Пауза между нажатиями + } + + // Если нужно, нажимаем кнопку для завершения ввода суммы + QList elements = XmlParser::parseUiDumpUsingQXml("ui_dump.xml"); + for (const UiElement &el: elements) { + if (el.text() == "ОК" && el.isClickable()) { + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по кнопке "ОК" + break; + } + } +} + +void Script::swipeToFindElement(const QString &searchText) { + bool found = false; + int attempts = 0; + + // Прокручиваем экран несколько раз, пока не найдем элемент или не превысим попытки + while (attempts < 5 && !found) { + QList elements = XmlParser::parseUiDumpUsingQXml("ui_dump.xml"); + + // Ищем нужный элемент на экране + for (const UiElement &el: elements) { + if (el.text() == searchText && el.isClickable()) { + qDebug() << "Element found: " << searchText; + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по найденному элементу + found = true; + break; + } + } + + // Если элемент не найден, выполняем свайп + if (!found) { + qDebug() << "Element not found, swiping..."; + AdbUtils::swipe(500, 1500, 500, 500); // Свайп вверх + attempts++; + QThread::sleep(2); // Пауза между свайпами + } + } + + if (!found) { + qWarning() << "Element " << searchText << " not found after " << attempts << " swipe attempts."; + } +} + +void Script::handleTransferScreen(QList &elements) { + // Проверка на экран перевода + if (XmlParser::isTransferScreen(elements)) { + qDebug() << "State: OnTransferScreen -> Looking for 'Enter amount' field or 'Confirm' button."; + + bool found = false; + + // Если поле для ввода суммы найдено, вводим сумму + for (const UiElement &el: elements) { + if (el.text() == "Сумма" && el.isClickable()) { + qDebug() << "Found 'Amount' input field"; + inputAmount("1000"); // Пример суммы + found = true; + break; + } + } + + if (!found) { + qWarning() << "'Amount' input field not found!"; + } + + // После ввода суммы ищем кнопку 'Оплатить' или 'Подтвердить' + for (const UiElement &el: elements) { + if (el.text() == "Оплатить" && el.isClickable()) { + int x = (el.x1() + el.x2()) / 2; + int y = (el.y1() + el.y2()) / 2; + AdbUtils::tap(x, y); // Тапаем по кнопке "Оплатить" + qDebug() << "Payment confirmed."; + break; + } + } + } +} + +void tapByText(const std::vector& ui, const std::string& text) { + for (const auto& el : ui) { + if (el.text == QString::fromStdString(text)) { + tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); + break; + } + } +} + +// Тап по полю ввода +void tapInputField(const std::vector& ui, const std::string& fieldText) { + for (const auto& el : ui) { + if (el.text == QString::fromStdString(fieldText)) { + tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); + break; + } + } +} + +// Тап по кнопке "Далее" +void tapNextButton(const std::vector& ui) { + for (const auto& el : ui) { + if (el.text == "Войти" || el.text == "Далее" || el.text == "Продолжить") { + tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); + break; + } + } +} + +bool handlePostLoginScreen(const std::vector& ui) { + for (const auto& el : ui) { + // Ищем баннеры и модальные окна, которые нужно закрыть + if (el.text == "Получайте новости с помощью" || el.text == "Не пропускайте специальные предложения и рекомендации") { + // Тапаем по пустой области + tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); + wait(900); + return true; + } + if (el.text == "Позже" || el.text == "Окей" || el.text == "Закрыть") { + // Тапаем по кнопке закрытия или кнопке "Позже" + tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); + wait(900); + return true; + } + } + return false; +} + +bool isMainScreen(const std::vector& ui) { + for (const auto& el : ui) { + if (el.text == "Счета и карты" || el.text == "Доступно баллов") { + return true; + } + } + return false; +} + +UiElement findCardByLast4Digits(const std::vector& ui, const std::string& last4digits) { + // Проходим по всем элементам UI, которые парсились из XML + for (const auto& el : ui) { + // Используем регулярное выражение для поиска 4 цифр в тексте + std::smatch m; + std::string str = el.text.toUtf8().constData(); // Преобразуем QString в std::string + + // Если в тексте найдено совпадение с регулярным выражением для 4 цифр + if (std::regex_search(str, m, std::regex(R"(\*\*\s?(\d{4}))"))) { + // Проверяем, совпадают ли последние 4 цифры с переданными + if (m[1] == last4digits) { + // Если нашли совпадение, возвращаем элемент + return el; + } + } + } + + // Если карта с такими последними цифрами не найдена, возвращаем пустой элемент + return {}; +} + +UiElement findPayButton(const std::vector& ui) { + for (const auto& el : ui) { + if (el.text == "Оплатить") { + return el; + } + } + return {}; +} + +void Script::stop() { + m_running = false; +} diff --git a/automation_script/Script.h b/automation_script/Script.h new file mode 100644 index 0000000..172d946 --- /dev/null +++ b/automation_script/Script.h @@ -0,0 +1,106 @@ +// +// Created by Misterio on 24.04.2025. +// + +#ifndef SCRIPT_H +#define SCRIPT_H +#include +#include +#include "AdbUtils.h" +#include "XmlParser.h" +#include "auto_payment/UiElement.h" +#include +#include + +class Script final : public QObject { + Q_OBJECT + +public: + explicit Script(QObject *parent = nullptr); + + ~Script() override; + +public slots: + void start(); // Старт фоновой работы + + void stop(); // Стоп фоновой работы + +signals: + void finished(); // Сигнал о завершении работы + +private: + bool m_running = true; + + // Функция для запуска скрипта с передачей параметров + void runScript(const QString &pin, const QString &last4digits); + + // Функции для определения различных экранов и действий + + // Проверка, что перед нами экран логина + bool isLoginScreen(const QList &ui); + + // Проверка, что перед нами экран ввода ПИН + bool isPinScreen(const QList &ui); + + // Получение списка кнопок с цифрами для ввода ПИН + std::vector getPinButtons(const QList &ui); + + // Тап по элементу с текстом + void tapByText(const QList &ui, const QString &text); + + // Тап по полю ввода + void tapInputField(const QList &ui, const QString &fieldText); + + // Тап по кнопке "Далее" и её вариациям + void tapNextButton(const QList &ui); + + // Обработка экрана с пост-логин баннерами или диалогами + bool handlePostLoginScreen(const QList &ui); + + // Проверка, что перед нами главный экран + bool isMainScreen(const QList &ui); + + // Тап по главному экрану + void tapMainScreen(const QList &ui); + + // Функция для поиска карты по последним 4 цифрам + UiElement findCardByLast4Digits(const QList &ui, const QString &last4digits); + + // Поиск кнопки "Оплатить" на экране + UiElement findPayButton(const QList &ui); + +private: + enum class State { + WaitingForPinInput, + ClosingBanner, + OnMainScreen, + SearchingCard, + OnCardDetails, + OnTransferScreen, + Idle + }; + + // Основной цикл скрипта + void processUiLoop(); + + void handleState(const QList &elements); + + static void inputPinCode(const QString &pinCode); + + void handleClosingBanner(QList &elements); + + static void swipeToFindElement(const QString &searchText); + + void handleTransferScreen(QList &elements); + + static void inputAmount(const QString &amount); + + // Утилиты для работы с ADB + AdbUtils adbUtils; + // Парсер XML UI дампов мобильного устройства + XmlParser xmlParser; + // Текущее состояние скрипта автоматизации + State currentState = State::Idle; +}; + +#endif //SCRIPT_H diff --git a/automation_script/XmlParser.cpp b/automation_script/XmlParser.cpp new file mode 100644 index 0000000..d8dcc05 --- /dev/null +++ b/automation_script/XmlParser.cpp @@ -0,0 +1,103 @@ +// +// Created by AmirS on 25.04.2025. +// + +#include "XmlParser.h" +#include +#include +#include + +XmlParser::XmlParser(QObject *parent) : QObject(parent) { +} + +QList XmlParser::parseUiDumpUsingQXml(const QString &filepath) { + QFile file(filepath); + QList elements; + if (!file.open(QIODevice::ReadOnly)) { + qDebug() << "Не удалось открыть XML файл:" << filepath; + return elements; + } + + QXmlStreamReader xmlReader(&file); + + while (!xmlReader.atEnd()) { + xmlReader.readNext(); + + if (xmlReader.hasError()) { + qDebug() << "Ошибка парсинга XML в линии" << xmlReader.lineNumber() << ":" + << xmlReader.errorString(); + qCritical() << "Ошибка парсинга XML: " << xmlReader.errorString() + << ", далее вернем пустой элемент"; + return {}; // Возвращаем пустоту в случае ошибки + } + + // Проверка на начало элемента + if (xmlReader.isStartElement()) { + if (xmlReader.name() == "node") { + // Создаем элемент из текущего узла + UiElement element = UiElement::fromXmlNode(xmlReader); + elements.append(element); + } + } + } + + if (xmlReader.hasError()) { + qWarning() << "Ошибка парсинга XML в конце:" << xmlReader.errorString(); + qDebug() << "Ошибка парсинга XML в конце:" << xmlReader.errorString(); + } + + return elements; +} + +bool XmlParser::isPinCodeScreen(const QList &elements) { + for (const UiElement &element: elements) { + // Проверка на наличие текста для ввода PIN + if (element.text().contains("Введите ПИН")) { + return true; + } + } + return false; +} + +bool XmlParser::isBannerOrDialog(const QList &elements) { + bool found = false; + + // 1. Проверка на наличие кнопок "Позже" и "Закрыть" + for (const UiElement &element: elements) { + // Проверка, что кнопка кликабельна и имеет текст "Позже" или "Закрыть" + if (element.isClickable()) { + if ((element.text() == "Позже" || element.text() == "Закрыть") && + element.className() == "android.widget.Button") { + found = true; + break; + } + } + } + + // 2. Если не нашли "Позже" или "Закрыть", ищем кнопку без текста в правой верхней части экрана + if (!found) { + for (const UiElement &element: elements) { + if (element.isClickable() && element.className() == "android.widget.Button" && + element.text().isEmpty()) { + // Проверка, что кнопка в правой верхней части экрана + // ~ Координаты в правом верхнем углу + if (element.x1() > 800 && element.y1() < 200) { + found = true; + break; + } + } + } + } + + return found; +} + +bool XmlParser::isMainScreen(const QList &elements) { + for (const UiElement &element: elements) { + // Проверка на кликабельную кнопку "Все продукты" + if (element.text().contains("Все продукты") && element.isClickable()) { + return true; + } + } + return false; +} diff --git a/automation_script/XmlParser.h b/automation_script/XmlParser.h new file mode 100644 index 0000000..39b1604 --- /dev/null +++ b/automation_script/XmlParser.h @@ -0,0 +1,24 @@ +#ifndef XMLPARSER_H +#define XMLPARSER_H + +#include +#include +#include +#include "auto_payment/UiElement.h" + +class XmlParser final : public QObject +{ + Q_OBJECT +public: + explicit XmlParser(QObject *parent = nullptr); + + static QList parseUiDumpUsingQXml(const QString &filepath); + static bool isPinCodeScreen(const QList& elements); + static bool isBannerOrDialog(const QList& elements); + static bool isMainScreen(const QList& elements); + +private: + static QList extractElementsFromXml(QXmlStreamReader &xmlReader); +}; + +#endif // XMLPARSER_H diff --git a/automation_script/adb_fun.cpp b/automation_script/adb_fun.cpp deleted file mode 100644 index e56ec12..0000000 --- a/automation_script/adb_fun.cpp +++ /dev/null @@ -1,61 +0,0 @@ -#include "adb_fun.h" -#include -#include -#include -#include -#include -#include -#include - -std::string exec(const std::string& cmd) { - std::array buffer{}; - std::string result; - FILE* pipe = _popen(cmd.c_str(), "r"); - if (!pipe) return "ERROR"; - while (fgets(buffer.data(), buffer.size(), pipe)) { - result += buffer.data(); - } - _pclose(pipe); - return result; -} - -bool checkDevices() { - std::string out = exec("adb devices"); - return out.find("\tdevice") != std::string::npos; -} - -bool startApp(const std::string& packageName) { - std::string cmd = "adb shell monkey -p " + packageName + " -c android.intent.category.LAUNCHER 1"; - return system(cmd.c_str()) == 0; -} - -bool tap(int x, int y) { - std::stringstream ss; - ss << "adb shell input tap " << x << " " << y; - return system(ss.str().c_str()) == 0; -} - -bool swipe(const int x1, const int y1, const int x2, const int y2) { - std::stringstream ss; - ss << "adb shell input swipe " << x1 << " " << y1 << " " << x2 << " " << y2; - return system(ss.str().c_str()) == 0; -} - -bool inputText(const std::string& text) { - std::stringstream ss; - ss << "adb shell input text \"" << text << "\""; - return system(ss.str().c_str()) == 0; -} - -bool takeScreenshot(const std::string& filename) { - system("mkdir screens 2>nul"); // Windows - std::string cmd = "adb exec-out screencap -p > screens/" + filename; - return system(cmd.c_str()) == 0; -} - -void log(const std::string& message) { - std::ofstream out("log.txt", std::ios::app); - std::time_t t = std::time(nullptr); - out << "[" << std::put_time(std::localtime(&t), "%H:%M:%S") << "] " << message << std::endl; - std::cout << message << std::endl; -} \ No newline at end of file diff --git a/automation_script/adb_fun.h b/automation_script/adb_fun.h deleted file mode 100644 index 9200165..0000000 --- a/automation_script/adb_fun.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef ADB_FUN_H -#define ADB_FUN_H - -#include -#include - -bool checkDevices(); -bool startApp(const std::string& packageName); -bool tap(int x, int y); -bool inputText(const std::string& text); -bool swipe(int x1, int y1, int x2, int y2); -bool takeScreenshot(const std::string& filename); - -void log(const std::string& message); - - - -#endif //ADB_FUN_H diff --git a/automation_script/script_worker.cpp b/automation_script/script_worker.cpp deleted file mode 100644 index 2043666..0000000 --- a/automation_script/script_worker.cpp +++ /dev/null @@ -1,240 +0,0 @@ -// -// Created by Misterio on 24.04.2025. -// - -#include "script_worker.h" -#include "adb_fun.h" -#include "xml_reader.h" -#include -#include -#include -#include - -// Вспомогательные функции - -void wait(const int milliseconds) { - std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); -} - -// Функция для запуска скрипта с передачей параметров -void runScriptWorker(const std::string& pin, const std::string& last4digits) { - log("Запуск скрипта для ru.rshb.dbo"); - - if (!checkDevices()) { - log("ADB: Нет устройств"); - qDebug("ADB: Нет устройств"); - return; - } - - if (!startApp("ru.rshb.dbo")) { - log("Не удалось запустить приложение"); - qDebug("Не удалось запустить приложение"); - return; - } - - wait(4500); - dumpAndPullUiXml("ui_01.xml"); - - auto ui = parseUiDumpUsingQXml("ui_01.xml"); - - if (isPinScreen(ui)) { - log("Экран PIN-кода"); - qDebug("Экран PIN-кода"); - - // Находим координаты цифр на экране и вводим ПИН - std::vector pinButtons = getPinButtons(ui); - for (size_t i = 0; i < pin.length(); ++i) { - char digit = pin[i]; - for (const auto& el : pinButtons) { - if (el.text == std::string(1, digit)) { - tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); - break; - } - } - } - wait(1000); - } - - // Пост-логин баннеры — цикл для обработки разных состояний показов модальных окон - bool postLoginScreenFound = false; - int attempts = 5; // Максимальное количество попыток парсинга xml - while (attempts-- > 0) { - dumpAndPullUiXml("ui_02.xml"); - auto ui2 = parseUiDumpUsingQXml("ui_02.xml"); - - // Закрытие баннеров или диалогов - if (handlePostLoginScreen(ui2)) { - postLoginScreenFound = true; - break; - } - - // Проверка, если мы на главном экране - if (isMainScreen(ui2)) { - log("Главный экран подтверждён"); - postLoginScreenFound = true; - break; - } - - // Ждем немного перед следующим запросом дампа экрана - wait(1000); - } - - if (!postLoginScreenFound) { - log("Не удалось определить пост-логин баннер."); - return; - } - - // Пролистывание до кнопки "Все продукты" - swipe(1000, 2000, 1000, 1300); - wait(500); - - // Нажимаем на кнопку "Все продукты" - dumpAndPullUiXml("ui_03.xml"); - auto ui3 = parseUiDumpUsingQXml("ui_03.xml"); - - // Ищем кнопку "Все продукты" и нажимаем на неё - tapByText(ui3, "Все продукты"); - wait(800); - - // Шаг 6: Выбор карты на экране "Все продукты" - // Запрашиваем дамп UI для поиска карт - dumpAndPullUiXml("ui_04.xml"); - auto ui4 = parseUiDumpUsingQXml("ui_04.xml"); - - // Определим, какую карту выбрать, например, с 4 цифрами "0823" - UiElement cardButton = findCardByLast4Digits(ui4, last4digits); - if (cardButton.text.isEmpty()) { - log("Не найдена карта с номером, содержащим " + last4digits); - return; - } - - // Нажимаем на карту - tap((cardButton.x1 + cardButton.x2) / 2, (cardButton.y1 + cardButton.y2) / 2); - wait(1000); - - // Нажимаем на кнопку "Оплатить" - // Запрашиваем дамп UI для поиска кнопки "Оплатить" - dumpAndPullUiXml("ui_05.xml"); - auto ui5 = parseUiDumpUsingQXml("ui_05.xml"); - - // Ищем кнопку "Оплатить" и нажимаем на неё - UiElement payBtn = findPayButton(ui5); - if (payBtn.text.isEmpty()) { - log("Не найдена кнопка 'Оплатить'"); - return; - } - - // Нажимаем на кнопку "Оплатить" - tap((payBtn.x1 + payBtn.x2) / 2, (payBtn.y1 + payBtn.y2) / 2); - log("Нажата кнопка 'Оплатить'"); - wait(1000); -} - -// Функции для определения экранов и действий - -bool isPinScreen(const std::vector& ui) { - for (const auto& el : ui) { - if (el.text == "Введите ПИН") { - return true; - } - } - return false; -} - -std::vector getPinButtons(const std::vector& ui) { - std::vector pinButtons; - for (const auto& el : ui) { - if (el.text.length() == 1 && std::isdigit(el.text[0].toLatin1())) { - pinButtons.push_back(el); // Собираем все элементы с цифрами - } - } - return pinButtons; -} - -void tapByText(const std::vector& ui, const std::string& text) { - for (const auto& el : ui) { - if (el.text == QString::fromStdString(text)) { - tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); - break; - } - } -} - -// Тап по полю ввода -void tapInputField(const std::vector& ui, const std::string& fieldText) { - for (const auto& el : ui) { - if (el.text == QString::fromStdString(fieldText)) { - tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); - break; - } - } -} - -// Тап по кнопке "Далее" -void tapNextButton(const std::vector& ui) { - for (const auto& el : ui) { - if (el.text == "Войти" || el.text == "Далее" || el.text == "Продолжить") { - tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); - break; - } - } -} - -bool handlePostLoginScreen(const std::vector& ui) { - for (const auto& el : ui) { - // Ищем баннеры и модальные окна, которые нужно закрыть - if (el.text == "Получайте новости с помощью" || el.text == "Не пропускайте специальные предложения и рекомендации") { - // Тапаем по пустой области - tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); - wait(900); - return true; - } - if (el.text == "Позже" || el.text == "Окей" || el.text == "Закрыть") { - // Тапаем по кнопке закрытия или кнопке "Позже" - tap((el.x1 + el.x2) / 2, (el.y1 + el.y2) / 2); - wait(900); - return true; - } - } - return false; -} - -bool isMainScreen(const std::vector& ui) { - for (const auto& el : ui) { - if (el.text == "Счета и карты" || el.text == "Доступно баллов") { - return true; - } - } - return false; -} - -UiElement findCardByLast4Digits(const std::vector& ui, const std::string& last4digits) { - // Проходим по всем элементам UI, которые парсились из XML - for (const auto& el : ui) { - // Используем регулярное выражение для поиска 4 цифр в тексте - std::smatch m; - std::string str = el.text.toUtf8().constData(); // Преобразуем QString в std::string - - // Если в тексте найдено совпадение с регулярным выражением для 4 цифр - if (std::regex_search(str, m, std::regex(R"(\*\*\s?(\d{4}))"))) { - // Проверяем, совпадают ли последние 4 цифры с переданными - if (m[1] == last4digits) { - // Если нашли совпадение, возвращаем элемент - return el; - } - } - } - - // Если карта с такими последними цифрами не найдена, возвращаем пустой элемент - return {}; -} - -UiElement findPayButton(const std::vector& ui) { - for (const auto& el : ui) { - if (el.text == "Оплатить") { - return el; - } - } - return {}; -} - diff --git a/automation_script/script_worker.h b/automation_script/script_worker.h deleted file mode 100644 index 3817fd7..0000000 --- a/automation_script/script_worker.h +++ /dev/null @@ -1,49 +0,0 @@ -// -// Created by Misterio on 24.04.2025. -// - -#ifndef SCRIPT_WORKER_H -#define SCRIPT_WORKER_H -#include -#include -#include "xml_reader.h" - -// Функция для запуска скрипта с передачей параметров -void runScriptWorker(const std::string& pin, const std::string& last4digits); - -// Функции для определения различных экранов и действий - -// Проверка, что перед нами экран логина -bool isLoginScreen(const std::vector& ui); - -// Проверка, что перед нами экран ввода ПИН -bool isPinScreen(const std::vector& ui); - -// Получение списка кнопок с цифрами для ввода ПИН -std::vector getPinButtons(const std::vector& ui); - -// Тап по элементу с текстом -void tapByText(const std::vector& ui, const std::string& text); - -// Тап по полю ввода -void tapInputField(const std::vector& ui, const std::string& fieldText); - -// Тап по кнопке "Далее" и её вариациям -void tapNextButton(const std::vector& ui); - -// Обработка экрана с пост-логин баннерами или диалогами -bool handlePostLoginScreen(const std::vector& ui); - -// Проверка, что перед нами главный экран -bool isMainScreen(const std::vector& ui); - -// Тап по главному экрану -void tapMainScreen(const std::vector& ui); - -// Функция для поиска карты по последним 4 цифрам -UiElement findCardByLast4Digits(const std::vector& ui, const std::string& last4digits); - -// Поиск кнопки "Оплатить" на экране -UiElement findPayButton(const std::vector& ui); - -#endif //SCRIPT_WORKER_H diff --git a/automation_script/xml_reader.cpp b/automation_script/xml_reader.cpp deleted file mode 100644 index 0f85dfa..0000000 --- a/automation_script/xml_reader.cpp +++ /dev/null @@ -1,79 +0,0 @@ -#include "xml_reader.h" - -#include -#include -#include -#include -#include - - -bool dumpAndPullUiXml(const std::string& localPath) { - int r1 = system("adb shell uiautomator dump /sdcard/view.xml"); - int r2 = system(("adb pull /sdcard/view.xml " + localPath).c_str()); - return r1 == 0 && r2 == 0; -} - -std::vector parseUiDumpUsingQXml(const QString& filePath) { - std::vector uiElements; - - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - qWarning() << "Не удалось открыть файл:" << filePath; - return uiElements; - } - - QXmlStreamReader xmlReader(&file); - UiElement currentElement; - - while (!xmlReader.atEnd()) { - xmlReader.readNext(); - - if (xmlReader.hasError()) { - qDebug() << "XML parsing error at line" << xmlReader.lineNumber() << ":" - << xmlReader.errorString(); - return {}; // Возвращаем пустой вектор в случае ошибки - } - - if (xmlReader.isStartElement()) { - qDebug() << "Found element:" << xmlReader.name(); - if (xmlReader.name() == "node") { - for (const QXmlStreamAttribute &attr : xmlReader.attributes()) { - if (attr.name() == "bounds") { - // Извлекаем координаты из bounds - QString bounds = attr.value().toString(); - QRegularExpression regex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); - QRegularExpressionMatch match = regex.match(bounds); - - if (match.hasMatch()) { - // Извлекаем координаты из регулярного выражения - currentElement.x1 = match.captured(1).toInt(); - currentElement.y1 = match.captured(2).toInt(); - currentElement.x2 = match.captured(3).toInt(); - currentElement.y2 = match.captured(4).toInt(); - } - } - if (attr.name() == "text") { - currentElement.text = attr.value().toString(); - } - if (attr.name() == "resource-id") { - currentElement.resourceId = attr.value().toString(); - } - } - uiElements.push_back(currentElement); - } - } - - // Пропускаем закрывающий тег - if (xmlReader.isEndElement()) { - qDebug() << "End element:" << xmlReader.name(); - } - } - - if (xmlReader.hasError()) { - qWarning() << "Final XML parsing error:" << xmlReader.errorString(); - } - - return uiElements; -} - - diff --git a/automation_script/xml_reader.h b/automation_script/xml_reader.h deleted file mode 100644 index d7038c8..0000000 --- a/automation_script/xml_reader.h +++ /dev/null @@ -1,15 +0,0 @@ -#ifndef XML_READER_H -#define XML_READER_H -#include - -struct UiElement { - int x1, y1, x2, y2; - QString text; - QString resourceId; -}; - -std::vector parseUiDumpUsingQXml(const QString &filePath); -bool dumpAndPullUiXml(const std::string& localPath); - - -#endif //XML_READER_H diff --git a/main.cpp b/main.cpp index 2d1500d..6bca3b2 100644 --- a/main.cpp +++ b/main.cpp @@ -8,7 +8,7 @@ #include "database/dao/CommonDataDAO.h" #include "db/AccountInfoScreener.h" #include "services/DeviceScreener.h" -#include "automation_script/script_worker.h" +#include "automation_script/Script.h" #include #include @@ -45,11 +45,31 @@ void setupWorkerAndThread(QCoreApplication &app) { thread->start(); } +void setupScriptAutoPaymentAndThread(QCoreApplication &app) { + auto *thread = new QThread; + auto *scriptWorker = new Script; + + // Перемещаем worker в новый поток thread. + // Это значит, что все слоты worker, вызываемые через connect(), теперь будут исполняться в этом фоновом потоке. + scriptWorker->moveToThread(thread); + // Когда поток стартует, запускаем start() + QObject::connect(thread, &QThread::started, scriptWorker, &Script::start); + // Когда работа (worker) завершена (emit finished()), останавливаем поток + QObject::connect(scriptWorker, &Script::finished, thread, &QThread::quit); + // Удаляем работу (worker) (deleteLater) + QObject::connect(scriptWorker, &Script::finished, scriptWorker, &QObject::deleteLater); + // Удаляем поток (deleteLater) + QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater); + + QObject::connect(&app, &QCoreApplication::aboutToQuit, [=]() { + scriptWorker->stop(); + }); + + thread->start(); +} + int main(int argc, char *argv[]) { - runScriptWorker("1211", "0823"); // Просто запускаем script - return 0; - QApplication app(argc, argv); // QList cards = AccountInfoScreener::parseCardData(QByteArray()); @@ -65,6 +85,7 @@ int main(int argc, char *argv[]) { // qDebug() << "Account info: " << AccountDAO::upsertAccount(info); // } + // setupScriptAutoPaymentAndThread(app); // setupWorkerAndThread(app); // diff --git a/models/auto_payment/UiElement.cpp b/models/auto_payment/UiElement.cpp new file mode 100644 index 0000000..153090e --- /dev/null +++ b/models/auto_payment/UiElement.cpp @@ -0,0 +1,47 @@ +// +// Created by AmirS on 26.04.2025. +// + +#include "UiElement.h" +#include +#include +#include + +UiElement::UiElement(const int x1, const int y1, const int x2, const int y2, QString text, QString className, + const bool clickable) + : m_x1(x1), m_y1(y1), m_x2(x2), m_y2(y2), m_text(std::move(text)), m_className(std::move(className)), + m_clickable(clickable) { +} + +static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); + +UiElement UiElement::fromXmlNode(const QXmlStreamReader &xmlReader) { + int x1 = 0, y1 = 0, x2 = 0, y2 = 0; + QString text, className; + bool clickable = false; + + if (xmlReader.attributes().hasAttribute("bounds")) { + QString bounds = xmlReader.attributes().value("bounds").toString(); + QRegularExpressionMatch match = boundsRegex.match(bounds); + if (match.hasMatch()) { + x1 = match.captured(1).toInt(); + y1 = match.captured(2).toInt(); + x2 = match.captured(3).toInt(); + y2 = match.captured(4).toInt(); + } + } + + if (xmlReader.attributes().hasAttribute("text")) { + text = xmlReader.attributes().value("text").toString(); + } + + if (xmlReader.attributes().hasAttribute("class")) { + className = xmlReader.attributes().value("class").toString(); + } + + if (xmlReader.attributes().hasAttribute("clickable")) { + clickable = xmlReader.attributes().value("clickable").toString() == "true"; + } + + return UiElement(x1, y1, x2, y2, text, className, clickable); +} diff --git a/models/auto_payment/UiElement.h b/models/auto_payment/UiElement.h new file mode 100644 index 0000000..275c5f1 --- /dev/null +++ b/models/auto_payment/UiElement.h @@ -0,0 +1,32 @@ +// +// Created by AmirS on 26.04.2025. +// + +#ifndef UIELEMENT_H +#define UIELEMENT_H + +#include + +class UiElement { +public: + explicit UiElement(int x1 = 0, int y1 = 0, int x2 = 0, int y2 = 0, + QString text = "", QString className = "", bool clickable = false); + + [[nodiscard]] int x1() const { return m_x1; } + [[nodiscard]] int y1() const { return m_y1; } + [[nodiscard]] int x2() const { return m_x2; } + [[nodiscard]] int y2() const { return m_y2; } + [[nodiscard]] QString text() const { return m_text; } + [[nodiscard]] QString className() const { return m_className; } + [[nodiscard]] bool isClickable() const { return m_clickable; } + + static UiElement fromXmlNode(const QXmlStreamReader &xmlReader); + +private: + int m_x1, m_y1, m_x2, m_y2; + QString m_text; + QString m_className; + bool m_clickable; +}; + +#endif // UIELEMENT_H