diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 75d425a..b3a6255 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -519,6 +519,14 @@ GetLastTransactionsScript::SearchResult GetLastTransactionsScript::searchOne( Node node; }; + // Guard от «залипшей» прокрутки: если свайп не двигает список (банковское + // приложение перестало скроллиться — частый случай при сбоях UI/жестов), + // видимые транзакции не меняются. Тогда листать до maxScrolls бессмысленно, + // а при десятках целей это упирается в 10-минутный watchdog → "Interrupted". + // Детектируем по сигнатуре видимого набора: 2 одинаковых подряд = прогресса нет. + QString prevViewSig; + int stallScrolls = 0; + for (int scroll = 0; scroll <= maxScrolls; ++scroll) { if (interrupted()) { m_error = "Interrupted"; @@ -1012,6 +1020,27 @@ GetLastTransactionsScript::SearchResult GetLastTransactionsScript::searchOne( } } + // Детект отсутствия прогресса прокрутки: сравниваем сигнатуру текущего + // видимого набора с предыдущей. Совпала — свайп ничего не сдвинул. + { + QStringList sigParts; + for (const VypiskaItem &it : items) { + sigParts << QString::number(it.amount, 'f', 2) + "|" + it.date + "|" + it.time; + } + const QString viewSig = sigParts.join(';') + "##" + groupDates.join(','); + if (!prevViewSig.isEmpty() && viewSig == prevViewSig) { + if (++stallScrolls >= 2) { + qWarning() << "[Dushanbe::GetLastTransactions] No scroll progress for" + << stallScrolls << "swipes (list stuck) — aborting search for amount=" + << QString::number(searchAmount, 'f', 2); + break; + } + } else { + stallScrolls = 0; + } + prevViewSig = viewSig; + } + // Скроллим вниз — свайп внутри области транзакций if (scroll < maxScrolls) { if (interrupted()) { m_error = "Interrupted"; return SearchResult::NotFound; } diff --git a/android/dushanbe/ScreenXmlParser.cpp b/android/dushanbe/ScreenXmlParser.cpp index b3e4038..0455658 100644 --- a/android/dushanbe/ScreenXmlParser.cpp +++ b/android/dushanbe/ScreenXmlParser.cpp @@ -164,25 +164,45 @@ Node ScreenXmlParser::findPinCodeEditText() { } bool ScreenXmlParser::isHomeScreen() { + // Нижняя навигация (Главная + соседняя вкладка) присутствует на ВСЕХ + // основных вкладках — Главная, История, Переводы, MiniApps — поэтому одного + // таб-бара недостаточно: экран «История/Операции» иначе ошибочно + // принимается за home, и скрипт пытается открыть боковое меню/листать не с + // той страницы (см. «Side menu not detected»). Требуем дополнительно + // уникальный для главной якорь и явно отсекаем экран истории. bool hasGlavnaya = false; bool hasBottomNav = false; + bool hasHomeAnchor = false; + bool isHistoryScreen = false; for (const Node &node : m_nodes) { - if (node.contentDesc.contains(QString::fromUtf8("Главная"))) { + const QString &d = node.contentDesc; + if (d.contains(QString::fromUtf8("Главная"))) { hasGlavnaya = true; } // Нижняя навигация — любая из соседних вкладок указывает, что мы на // основной странице приложения, а не на splash/PIN/диалоге. - if (node.contentDesc == QString::fromUtf8("Переводы") || - node.contentDesc == QString::fromUtf8("Сервисы") || - node.contentDesc == QString::fromUtf8("MiniApps") || - node.contentDesc == QString::fromUtf8("История")) { + if (d == QString::fromUtf8("Переводы") || + d == QString::fromUtf8("Сервисы") || + d == QString::fromUtf8("MiniApps") || + d == QString::fromUtf8("История")) { hasBottomNav = true; } - if (hasGlavnaya && hasBottomNav) { - return true; + // Уникальные для главного экрана элементы (нет на Истории/Переводах): + // заголовок «Мини приложения», плитки переводов «DC по номеру …» + // (в 3.2.38 — «DC (по номеру …)») и карточка баланса «TJS\n<дата>». + if (d == QString::fromUtf8("Мини приложения") || + d.startsWith(QString::fromUtf8("DC по номеру")) || + d.startsWith(QString::fromUtf8("DC (по номеру")) || + (d.startsWith(QStringLiteral("TJS")) && d.contains('\n'))) { + hasHomeAnchor = true; + } + // Маркеры экрана «История/Операции» — на главном их нет. + if (d == QString::fromUtf8("Выписка") || + d == QString::fromUtf8("Операции")) { + isHistoryScreen = true; } } - return false; + return hasGlavnaya && hasBottomNav && hasHomeAnchor && !isHistoryScreen; } std::optional ScreenXmlParser::parseBalanceFromHomeScreenOpt() { diff --git a/services/EventHandler.cpp b/services/EventHandler.cpp index 52c46ad..6d01e4a 100644 --- a/services/EventHandler.cpp +++ b/services/EventHandler.cpp @@ -78,7 +78,7 @@ void EventHandler::start() { if (e.sentToServer) continue; EventDAO::updateEvent(e.id, EventStatus::Error, "App restarted"); EventDAO::markSentToServer(e.id); - sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, -1, e.transactionId, e.accountId); + sendTaskError(eventTypeToString(e.type), e.externalId, e.bankName, "App restarted, event was pending", {}, e.id, e.transactionId, e.accountId); qDebug() << "[EventHandler] Closed stale event id:" << e.id; } } @@ -127,7 +127,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { if (eventType == EventType::FetchProfile) { const QList cancelled = EventDAO::cancelWaitingByMaterial(materialId, EventType::FetchProfile); for (const EventInfo &c : cancelled) { - sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, -1, c.transactionId, c.accountId); + sendTaskError(eventTypeToString(c.type), c.externalId, c.bankName, "Cancelled: replaced by new task", {}, c.id, c.transactionId, c.accountId); EventDAO::markSentToServer(c.id); } } @@ -168,7 +168,7 @@ void EventHandler::onTasksReceived(const QJsonArray &tasks) { if (!onlineDevices.contains(account.deviceId)) { qWarning() << "[EventHandler] Device offline for material_id:" << materialId << "device:" << account.deviceId; EventDAO::updateEvent(eventId, EventStatus::Error, "Device is offline"); - sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, -1, incomingTransactionId, account.id); + sendTaskError(typeStr, materialId, bankName, "Device is offline", {}, eventId, incomingTransactionId, account.id); } } @@ -417,7 +417,7 @@ void EventHandler::updateEventThread(const QString &key, const EventInfo &event, declined["status"] = QStringLiteral("declined"); declined["extra"] = QJsonObject(); declined["created_at"] = QDateTime::currentDateTimeUtc().toString(Qt::ISODate); - sendTaskResult(QStringLiteral("CREATE_TRANSACTION"), event.externalId, event.bankName, declined, -1); + sendTaskResult(QStringLiteral("CREATE_TRANSACTION"), event.externalId, event.bankName, declined, event.id); } EventDAO::markSentToServer(event.id); @@ -774,7 +774,7 @@ void EventHandler::startThreadForTask(const MaterialInfo &account, const EventIn const QString logMsg = QString("[%1] bank=%2 material=%3: Script timeout") .arg(eventTypeToString(event.type), event.bankName, event.externalId); GeneralLogDAO::insert("CRITICAL", "EventHandler", logMsg); - sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, timeoutMsg, {}, -1, event.transactionId, event.accountId); + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, timeoutMsg, {}, event.id, event.transactionId, event.accountId); EventDAO::markSentToServer(event.id); // Убиваем банковское приложение на устройстве после таймаута @@ -811,7 +811,7 @@ void EventHandler::processNextTask(const QString &deviceId) { if (waitSecs > 5 * 60) { qWarning() << "[EventHandler] Task expired after" << waitSecs << "s, event id:" << event.id; EventDAO::updateEvent(event.id, EventStatus::Error, "Task expired: waited " + QString::number(waitSecs) + "s"); - sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, -1, event.transactionId, event.accountId); + sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, "Task expired: not started within 5 minutes", {}, event.id, event.transactionId, event.accountId); continue; } @@ -821,7 +821,7 @@ void EventHandler::processNextTask(const QString &deviceId) { qWarning() << "[EventHandler] Bank profile disabled for" << account.appCode << "device:" << deviceId; EventDAO::updateEvent(event.id, EventStatus::Error, "Bank profile is disabled"); sendTaskError(eventTypeToString(event.type), event.externalId, event.bankName, - "Bank profile is disabled: " + account.appCode, {}, -1, event.transactionId, event.accountId); + "Bank profile is disabled: " + account.appCode, {}, event.id, event.transactionId, event.accountId); continue; } diff --git a/services/net/NetworkService.h b/services/net/NetworkService.h index 91af1b9..4e55137 100644 --- a/services/net/NetworkService.h +++ b/services/net/NetworkService.h @@ -194,7 +194,7 @@ private: bool m_unhealthyEmitted = false; // чтобы не спамить syncHealthChanged static constexpr int kSyncFailureThreshold = 3; static constexpr int kPeriodicSyncMs = 60 * 1000; - static constexpr int kPingProfileMs = 5 * 1000; + static constexpr int kPingProfileMs = 15 * 1000; // Heartbeat (ping) bool m_pingEnabled = false;