Refine pin code validation logic, add device timezone support in transaction scripts, and improve XML node logging for error handling.
This commit is contained in:
parent
7465233b34
commit
2febc1e78c
@ -489,6 +489,14 @@ QSet<QString> AdbUtils::getListApps(const QString &deviceId) {
|
||||
return apps;
|
||||
}
|
||||
|
||||
QString AdbUtils::getDeviceTimezone(const QString &deviceId) {
|
||||
QString out;
|
||||
if (!startProcess(adbPath(), {"-s", deviceId, "shell", "getprop", "persist.sys.timezone"}, &out)) {
|
||||
return {};
|
||||
}
|
||||
return out.trimmed();
|
||||
}
|
||||
|
||||
void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) {
|
||||
const QString adb = adbPath();
|
||||
|
||||
|
||||
@ -49,6 +49,9 @@ public:
|
||||
|
||||
static QSet<QString> getListApps(const QString &deviceId);
|
||||
|
||||
// Возвращает таймзону устройства (напр. "Asia/Bangkok"), либо пустую строку при ошибке
|
||||
static QString getDeviceTimezone(const QString &deviceId);
|
||||
|
||||
private:
|
||||
static QString adbDir();
|
||||
static bool startProcess(const QString &program, const QStringList &args,
|
||||
|
||||
@ -316,6 +316,36 @@ void GetLastTransactionsScript::doStart() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Переключаемся на вкладку "Выписка" (по умолчанию открыта "Операции")
|
||||
Node vypiskaBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Выписка"));
|
||||
if (vypiskaBtn.isEmpty()) {
|
||||
m_error = "Cannot find 'Выписка' tab: " + m_deviceId;
|
||||
qWarning() << m_error;
|
||||
AdbUtils::tryToKillApplication(m_deviceId, app.package);
|
||||
emit finishedWithResult(m_error);
|
||||
return;
|
||||
}
|
||||
qDebug() << "[Dushanbe::GetLastTransactions] Switching to 'Выписка' tab";
|
||||
AdbUtils::makeTap(m_deviceId, vypiskaBtn.x(), vypiskaBtn.y());
|
||||
QThread::msleep(3000);
|
||||
|
||||
// Сохраняем дамп Выписки для анализа формата
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
|
||||
// Закрываем возможный диалог "Ошибка" на Выписке
|
||||
if (!xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("Ошибка")).isEmpty()) {
|
||||
Node okBtn = xmlScreenParser.findNodeByContentDesc(QString::fromUtf8("ОК"));
|
||||
if (!okBtn.isEmpty()) {
|
||||
qDebug() << "[Dushanbe::GetLastTransactions] Error dialog on Выписка, tapping OK";
|
||||
AdbUtils::makeTap(m_deviceId, okBtn.x(), okBtn.y());
|
||||
QThread::msleep(1000);
|
||||
}
|
||||
const int x = device.screenWidth / 2;
|
||||
AdbUtils::makeSwipe(m_deviceId, x, device.screenHeight / 4, x, device.screenHeight * 3 / 4);
|
||||
QThread::msleep(3000);
|
||||
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
|
||||
}
|
||||
|
||||
// Находим accountId
|
||||
int accountId = -1;
|
||||
const QList<MaterialInfo> accounts = MaterialDAO::getAccounts(device.id, m_appCode);
|
||||
@ -443,9 +473,9 @@ void GetLastTransactionsScript::doStart() {
|
||||
saveTx.fee = detail.fee;
|
||||
saveTx.bankName = m_appCode;
|
||||
saveTx.bankTime = detail.date + " " + detail.time;
|
||||
saveTx.bankTrExternalId = detail.operationId;
|
||||
saveTx.bankTrExternalId = "dushanbe_" + detail.date + "_" + detail.time;
|
||||
saveTx.description = detail.provider;
|
||||
saveTx.phone = detail.receiverAccount;
|
||||
saveTx.phone = "";
|
||||
saveTx.name = detail.receiverAccount;
|
||||
saveTx.status = detail.status.contains(QString::fromUtf8("Успешн"))
|
||||
? TransactionStatus::Complete : TransactionStatus::Unknown;
|
||||
|
||||
@ -134,8 +134,8 @@ bool CommonScript::runAppAndGoToHomeScreen(
|
||||
|
||||
// Проверяем, что это экран ввода пин-кода
|
||||
if (!findAndInputPincode) {
|
||||
if (const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||
pincode.size() == 4) {
|
||||
const QList<Node> pincode = xmlScreenParser.tryToFindPinCode(pinCode, "Введите ПИН");
|
||||
if (pincode.size() == pinCode.length() && pincode.size() >= 4) {
|
||||
// чтобы пробудить экран
|
||||
if (Node textPinCodeNode = xmlScreenParser.findTextNode("Введите ПИН"); !textPinCodeNode.isEmpty()) {
|
||||
AdbUtils::makeTap(deviceId, textPinCodeNode.x(), textPinCodeNode.y());
|
||||
|
||||
@ -203,17 +203,21 @@ bool GetLastTransactionsScript::navigateToTransactionList(const DeviceInfo &devi
|
||||
}
|
||||
|
||||
void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int accountId) {
|
||||
const QTimeZone msk("Europe/Moscow");
|
||||
// Время в Ozon показывается в таймзоне устройства, а не в MSK
|
||||
const QString tzName = AdbUtils::getDeviceTimezone(m_deviceId);
|
||||
QTimeZone deviceTz(tzName.toUtf8());
|
||||
if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow");
|
||||
const int maxScrolls = 30;
|
||||
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
|
||||
|
||||
// Целевая дата в МСК для поиска заголовка дня в списке
|
||||
const QDateTime searchMsk = m_searchTime.toTimeZone(msk);
|
||||
const QDate searchDate = searchMsk.date();
|
||||
// Целевая дата в таймзоне устройства для поиска заголовка дня в списке
|
||||
const QDateTime searchLocal = m_searchTime.toTimeZone(deviceTz);
|
||||
const QDate searchDate = searchLocal.date();
|
||||
|
||||
qDebug() << "[FetchTransactions] Searching: amount=" << m_searchAmount
|
||||
<< "time=" << m_searchTime.toString(Qt::ISODate)
|
||||
<< "date(MSK)=" << searchDate.toString("dd.MM.yyyy");
|
||||
<< "tz=" << tzName
|
||||
<< "date(local)=" << searchDate.toString("dd.MM.yyyy");
|
||||
|
||||
// ── Шаг 1: скроллим до нужной даты ──────────────────────────────
|
||||
bool dateFound = false;
|
||||
@ -231,18 +235,28 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
{"сентября", 9}, {"октября", 10}, {"ноября", 11}, {"декабря", 12}
|
||||
};
|
||||
|
||||
const QDate today = QDateTime::currentDateTimeUtc().toTimeZone(deviceTz).date();
|
||||
bool hasOlderDate = false;
|
||||
for (const Node &node : xmlScreenParser.getNodes()) {
|
||||
if (node.className != "android.widget.TextView") continue;
|
||||
const QString t = node.text.trimmed();
|
||||
// Парсим "4 января, вс" → QDate
|
||||
static const QRegularExpression dateRx(R"((\d{1,2})\s+(\S+),\s+\S+)");
|
||||
const auto match = dateRx.match(t);
|
||||
if (!match.hasMatch()) continue;
|
||||
const int day = match.captured(1).toInt();
|
||||
const int month = monthMap.value(match.captured(2), 0);
|
||||
if (month == 0) continue;
|
||||
const QDate headerDate(searchDate.year(), month, day);
|
||||
QDate headerDate;
|
||||
|
||||
// Относительные заголовки "Сегодня" / "Вчера"
|
||||
if (t.compare("Сегодня", Qt::CaseInsensitive) == 0) {
|
||||
headerDate = today;
|
||||
} else if (t.compare("Вчера", Qt::CaseInsensitive) == 0) {
|
||||
headerDate = today.addDays(-1);
|
||||
} else {
|
||||
// Абсолютный формат "4 января, вс"
|
||||
static const QRegularExpression dateRx(R"((\d{1,2})\s+(\S+),\s+\S+)");
|
||||
const auto match = dateRx.match(t);
|
||||
if (!match.hasMatch()) continue;
|
||||
const int day = match.captured(1).toInt();
|
||||
const int month = monthMap.value(match.captured(2), 0);
|
||||
if (month == 0) continue;
|
||||
headerDate = QDate(searchDate.year(), month, day);
|
||||
}
|
||||
if (!headerDate.isValid()) continue;
|
||||
|
||||
if (headerDate == searchDate) {
|
||||
@ -356,7 +370,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
}
|
||||
QDateTime txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm");
|
||||
if (!txTime.isValid()) txTime = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm");
|
||||
if (txTime.isValid()) txTime.setTimeZone(msk);
|
||||
if (txTime.isValid()) txTime.setTimeZone(deviceTz);
|
||||
|
||||
// Проверяем что дата и время совпадают (±1 час от m_searchTime)
|
||||
if (txTime.isValid()) {
|
||||
@ -370,7 +384,7 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
const qint64 diffSecs = qAbs(txTime.toUTC().secsTo(m_searchTime.toUTC()));
|
||||
if (diffSecs > 3600) {
|
||||
qDebug() << "[FetchTransactions] Time mismatch:" << dtStr
|
||||
<< "expected" << searchMsk.toString("dd.MM.yyyy, HH:mm")
|
||||
<< "expected" << searchLocal.toString("dd.MM.yyyy, HH:mm")
|
||||
<< "diff=" << diffSecs << "sec";
|
||||
AdbUtils::goBack(m_deviceId);
|
||||
QThread::msleep(1000);
|
||||
@ -434,7 +448,9 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
tx.amount = parsed.amount;
|
||||
tx.bankName = m_appCode;
|
||||
tx.bankTime = dtStr;
|
||||
tx.bankTrExternalId = externalId;
|
||||
tx.bankTrExternalId = externalId.isEmpty()
|
||||
? QString("ozon_") + QString(dtStr).remove(',').trimmed().replace(' ', '_')
|
||||
: externalId;
|
||||
tx.status = TransactionStatus::Complete;
|
||||
tx.timestamp = DateUtils::getUtcNow();
|
||||
if (txTime.isValid()) tx.completeTime = txTime;
|
||||
@ -474,7 +490,9 @@ void GetLastTransactionsScript::searchTransaction(const DeviceInfo &device, int
|
||||
}
|
||||
|
||||
void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device, int accountId) {
|
||||
const QTimeZone msk("Europe/Moscow");
|
||||
const QString tzName = AdbUtils::getDeviceTimezone(m_deviceId);
|
||||
QTimeZone deviceTz(tzName.toUtf8());
|
||||
if (!deviceTz.isValid()) deviceTz = QTimeZone("Europe/Moscow");
|
||||
const int navBarTop = device.screenHeight - (device.screenHeight / 10);
|
||||
|
||||
// Ждём появления транзакций
|
||||
@ -581,7 +599,7 @@ void GetLastTransactionsScript::scanRecentTransactions(const DeviceInfo &device,
|
||||
QDateTime parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy, HH:mm");
|
||||
if (!parsed.isValid()) parsed = QDateTime::fromString(dtStr, "dd.MM.yyyy,HH:mm");
|
||||
if (parsed.isValid()) {
|
||||
parsed.setTimeZone(msk);
|
||||
parsed.setTimeZone(deviceTz);
|
||||
tx.completeTime = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -427,6 +427,10 @@ QString ScreenXmlParser::parseProfilePhone() {
|
||||
|
||||
QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
|
||||
const QString cleanPin = QString(pinCode).remove(' ');
|
||||
if (cleanPin.isEmpty()) {
|
||||
qWarning() << "[findPinCodeNodes] pinCode is empty — check bank_profiles.pin_code in DB";
|
||||
return {};
|
||||
}
|
||||
// Способ 1: ищем кликабельные ноды с текстом-цифрой (0-9)
|
||||
// Поддерживаем форматы: "1", "key 1", и т.п.
|
||||
QMap<QString, Node> digitMap;
|
||||
@ -499,8 +503,11 @@ QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
|
||||
|
||||
// Сначала квадратные, потом любые
|
||||
QList<Node> candidates = fuzzyGroup(true);
|
||||
if (candidates.size() < 10)
|
||||
qDebug() << "[findPinCodeNodes] fallback square candidates:" << candidates.size();
|
||||
if (candidates.size() < 10) {
|
||||
candidates = fuzzyGroup(false);
|
||||
qDebug() << "[findPinCodeNodes] fallback any candidates:" << candidates.size();
|
||||
}
|
||||
|
||||
if (candidates.size() < 10) {
|
||||
// Дебаг: выводим все clickable ноды и их размеры
|
||||
@ -575,8 +582,12 @@ QList<Node> ScreenXmlParser::findPinCodeNodes(const QString &pinCode) {
|
||||
for (const QChar &c : cleanPin) {
|
||||
if (const QString d = QString(c); digitMap.contains(d)) {
|
||||
result.append(digitMap[d]);
|
||||
} else {
|
||||
qWarning() << "[findPinCodeNodes] fallback: digit not in digitMap:" << d;
|
||||
}
|
||||
}
|
||||
qDebug() << "[findPinCodeNodes] fallback result size:" << result.size()
|
||||
<< "digitMap size:" << digitMap.size();
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user