Refactor payment confirmation logic: add locateConfirmButton with prioritized search (color → OCR → geometry), enhance tap precision
This commit is contained in:
parent
e760f2bb5f
commit
9f23991c61
@ -97,6 +97,48 @@ static QString recognizeOperationId(const QByteArray &pngData) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Координаты кнопки "Подтверждаю" на bottom sheet подтверждения платежа.
|
||||
// Compose flatten-ит весь sheet в одну accessibility-ноду — отдельного узла
|
||||
// кнопки в дампе нет, поэтому позицию ищем по скриншоту тремя способами по
|
||||
// убыванию надёжности:
|
||||
// 1) цвет — оранжевая sticky-кнопка RGB(255,117,40); не зависит от текста,
|
||||
// контраста, языка и разрешения (самый стабильный путь);
|
||||
// 2) OCR "Подтверждаю" — если вдруг тема/цвет кнопки иные;
|
||||
// 3) геометрия — центр кнопки ≈ 6% высоты экрана над низом sheet'a; считаем
|
||||
// относительно высоты, а не в абсолютных пикселях, чтобы работать на
|
||||
// любом разрешении.
|
||||
// `confirmNode` — нода sheet'a (на весь экран), `height` — высота экрана.
|
||||
static QPoint locateConfirmButton(const QString &deviceId,
|
||||
const Node &confirmNode,
|
||||
int height) {
|
||||
// Окно поиска — нижняя половина sheet'a, где живёт кнопка. Sheet-нода
|
||||
// растянута на весь экран, поэтому y1/y2 сами по себе поиск не сужают.
|
||||
const int yMin = (confirmNode.y1() + confirmNode.y2()) / 2;
|
||||
const int yMax = confirmNode.y2();
|
||||
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
|
||||
// 1) Цвет.
|
||||
QPoint tap = OcrUtils::findColorButtonCenter(screen, 255, 117, 40, 55, yMin, yMax);
|
||||
if (tap.x() >= 0) {
|
||||
qDebug() << "[Dushanbe::PayByCard] confirm button via color at" << tap;
|
||||
return tap;
|
||||
}
|
||||
|
||||
// 2) OCR.
|
||||
tap = OcrUtils::findTextCenter(screen, QString::fromUtf8("Подтверждаю"), yMin, yMax);
|
||||
if (tap.x() >= 0) {
|
||||
qDebug() << "[Dushanbe::PayByCard] confirm button via OCR at" << tap;
|
||||
return tap;
|
||||
}
|
||||
|
||||
// 3) Геометрия: кнопка у самого низа sheet'a. Целимся ВНИЗ к y2, а не вверх.
|
||||
const int sheetH = confirmNode.y2() - confirmNode.y1();
|
||||
const int offset = qMax(sheetH * 6 / 100, height / 16);
|
||||
tap = QPoint(confirmNode.x(), confirmNode.y2() - offset);
|
||||
qDebug() << "[Dushanbe::PayByCard] confirm button geometry fallback at" << tap;
|
||||
return tap;
|
||||
}
|
||||
|
||||
namespace Dushanbe {
|
||||
|
||||
PayByCardScript::PayByCardScript(
|
||||
@ -383,23 +425,8 @@ void PayByCardScript::makePayment(
|
||||
QString::fromUtf8("Подтверждение платежа"), true);
|
||||
if (!confirmNode.isEmpty() &&
|
||||
confirmNode.contentDesc.contains(QString::fromUtf8("Подтверждаю"))) {
|
||||
// Compose flatten-ит весь bottom sheet в одну accessibility-кнопку,
|
||||
// отдельного узла "Подтверждаю" в дампе нет — координаты берём OCR-ом
|
||||
// по скриншоту. Если OCR не нашёл — fallback на геометрию (центр
|
||||
// нижней четверти sheet, выше системного nav-бара).
|
||||
// Compose отдаёт sheet как одну ноду на весь экран ([0,0][720,1640]),
|
||||
// поэтому confirmNode.y1/y2 не сужают поиск — OCR сканирует весь
|
||||
// скриншот и ловит ложные совпадения. Ограничиваем нижней половиной.
|
||||
const int ocrYMin = (confirmNode.y1() + confirmNode.y2()) / 2;
|
||||
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
QPoint tap = OcrUtils::findTextCenter(screen,
|
||||
QString::fromUtf8("Подтверждаю"),
|
||||
ocrYMin, confirmNode.y2());
|
||||
if (tap.x() < 0) {
|
||||
const int tapY = confirmNode.y2() - qMax(150, height / 14);
|
||||
tap = QPoint(confirmNode.x(), tapY);
|
||||
qDebug() << "[Dushanbe::PayByCard] OCR miss, fallback tap at" << tap;
|
||||
}
|
||||
// Позицию кнопки ищем: цвет → OCR → геометрия (см. locateConfirmButton).
|
||||
const QPoint tap = locateConfirmButton(deviceId, confirmNode, height);
|
||||
qDebug() << "[Dushanbe::PayByCard] Tapping 'Подтверждаю' at" << tap;
|
||||
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
|
||||
confirmed = true;
|
||||
@ -487,20 +514,9 @@ void PayByCardScript::makePayment(
|
||||
++confirmRetries;
|
||||
qDebug() << "[Dushanbe::PayByCard] Confirm sheet still visible, re-tap"
|
||||
<< confirmRetries << "/" << maxConfirmRetries;
|
||||
const int ocrYMin = (retryConfirm.y1() + retryConfirm.y2()) / 2;
|
||||
const QByteArray screen = AdbUtils::captureScreenshotBytes(deviceId);
|
||||
QPoint tap = OcrUtils::findTextCenter(screen,
|
||||
QString::fromUtf8("Подтверждаю"),
|
||||
ocrYMin, retryConfirm.y2());
|
||||
if (tap.x() < 0) {
|
||||
// Варьируем Y: первый retry — выше fallback на 100px,
|
||||
// второй — ещё на 100px выше. Compose-кнопка действия
|
||||
// обычно на y2 - 150..350; перебираем диапазон.
|
||||
const int extra = 100 * confirmRetries;
|
||||
const int tapY = retryConfirm.y2() - qMax(150 + extra, height / 14 + extra);
|
||||
tap = QPoint(retryConfirm.x(), tapY);
|
||||
qDebug() << "[Dushanbe::PayByCard] OCR miss, fallback tap at" << tap;
|
||||
}
|
||||
// Та же логика, что и на первом тапе: цвет → OCR → геометрия.
|
||||
const QPoint tap = locateConfirmButton(deviceId, retryConfirm, height);
|
||||
qDebug() << "[Dushanbe::PayByCard] Re-tapping 'Подтверждаю' at" << tap;
|
||||
AdbUtils::makeTap(deviceId, tap.x(), tap.y());
|
||||
QThread::msleep(4000);
|
||||
continue;
|
||||
|
||||
@ -202,4 +202,89 @@ QPoint findTextCenter(const QByteArray &png,
|
||||
return result;
|
||||
}
|
||||
|
||||
QPoint findColorButtonCenter(const QByteArray &png,
|
||||
int targetR,
|
||||
int targetG,
|
||||
int targetB,
|
||||
int tolerance,
|
||||
int yMin,
|
||||
int yMax) {
|
||||
if (png.isEmpty()) return {-1, -1};
|
||||
|
||||
QElapsedTimer timer;
|
||||
timer.start();
|
||||
|
||||
Pix *src = pixReadMem(reinterpret_cast<const l_uint8 *>(png.constData()),
|
||||
static_cast<size_t>(png.size()));
|
||||
if (!src) {
|
||||
qWarning() << "[OcrUtils] findColorButtonCenter: pixReadMem failed";
|
||||
return {-1, -1};
|
||||
}
|
||||
// Приводим к 32bpp, чтобы pixGetRGBPixel работал независимо от исходной
|
||||
// глубины (PNG со скриншота бывает 24/32 bpp).
|
||||
Pix *pix = pixConvertTo32(src);
|
||||
pixDestroy(&src);
|
||||
if (!pix) return {-1, -1};
|
||||
|
||||
const int W = pixGetWidth(pix);
|
||||
const int H = pixGetHeight(pix);
|
||||
const int y0 = qBound(0, yMin, H);
|
||||
const int y1 = (yMax > 0 && yMax <= H) ? yMax : H;
|
||||
|
||||
// Для каждой строки считаем число пикселей целевого цвета и их X-диапазон.
|
||||
QVector<int> rowCount(H, 0), rowMinX(H, -1), rowMaxX(H, -1);
|
||||
for (int y = y0; y < y1; ++y) {
|
||||
int cnt = 0, mn = W, mx = -1;
|
||||
for (int x = 0; x < W; ++x) {
|
||||
l_int32 r, g, b;
|
||||
pixGetRGBPixel(pix, x, y, &r, &g, &b);
|
||||
if (qAbs(r - targetR) <= tolerance &&
|
||||
qAbs(g - targetG) <= tolerance &&
|
||||
qAbs(b - targetB) <= tolerance) {
|
||||
++cnt;
|
||||
if (x < mn) mn = x;
|
||||
if (x > mx) mx = x;
|
||||
}
|
||||
}
|
||||
rowCount[y] = cnt;
|
||||
rowMinX[y] = mn;
|
||||
rowMaxX[y] = mx;
|
||||
}
|
||||
|
||||
// Кнопка — широкая сплошная полоса: ищем самую длинную серию строк, где
|
||||
// заполнение целевым цветом ≥ 25% ширины. Мелкие цветные иконки в статус-
|
||||
// баре такой порог не проходят.
|
||||
const int minRowFill = qMax(1, W / 4);
|
||||
int bestStart = -1, bestLen = 0, curStart = -1;
|
||||
for (int y = y0; y <= y1; ++y) {
|
||||
const bool filled = (y < y1) && (rowCount[y] >= minRowFill);
|
||||
if (filled) {
|
||||
if (curStart < 0) curStart = y;
|
||||
} else if (curStart >= 0) {
|
||||
const int len = y - curStart;
|
||||
if (len > bestLen) { bestLen = len; bestStart = curStart; }
|
||||
curStart = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Полоса должна быть не тоньше ~1% высоты — отсекаем шум/тонкие разделители.
|
||||
if (bestStart < 0 || bestLen < qMax(8, H / 100)) {
|
||||
pixDestroy(&pix);
|
||||
qDebug() << "[OcrUtils] findColorButtonCenter: no button band in"
|
||||
<< timer.elapsed() << "ms";
|
||||
return {-1, -1};
|
||||
}
|
||||
|
||||
int xmin = W, xmax = -1;
|
||||
for (int y = bestStart; y < bestStart + bestLen; ++y) {
|
||||
if (rowMinX[y] >= 0) { xmin = qMin(xmin, rowMinX[y]); xmax = qMax(xmax, rowMaxX[y]); }
|
||||
}
|
||||
pixDestroy(&pix);
|
||||
|
||||
const QPoint center((xmin + xmax) / 2, bestStart + bestLen / 2);
|
||||
qDebug() << "[OcrUtils] findColorButtonCenter → (" << center.x() << ","
|
||||
<< center.y() << ") band h=" << bestLen << "in" << timer.elapsed() << "ms";
|
||||
return center;
|
||||
}
|
||||
|
||||
} // namespace OcrUtils
|
||||
@ -19,4 +19,25 @@ QPoint findTextCenter(const QByteArray &png,
|
||||
int yMin = 0,
|
||||
int yMax = 0);
|
||||
|
||||
// Находит крупную сплошную кнопку заданного цвета (по умолчанию — оранжевая
|
||||
// sticky-action кнопка Dushanbe "Подтверждаю"/"Оплатить", RGB(255,117,40)) и
|
||||
// возвращает центр её bounding box в координатах экрана (= пиксели скриншота).
|
||||
//
|
||||
// В отличие от findTextCenter не зависит от текста, контраста и языка: OCR на
|
||||
// белом-по-оранжевому ненадёжен, а сплошной цветной прямоугольник ловится
|
||||
// стабильно. Алгоритм — поиск самой длинной вертикальной полосы строк, где
|
||||
// доля пикселей целевого цвета превышает порог по ширине; это отсекает мелкие
|
||||
// цветные иконки в статус-баре и ловит именно широкую кнопку. Координаты
|
||||
// относительные по построению, поэтому работает на любом разрешении.
|
||||
//
|
||||
// `yMin`/`yMax` — необязательное окно по Y (координаты экрана), 0/0 — весь
|
||||
// кадр. Возвращает QPoint(-1, -1), если подходящей полосы не найдено.
|
||||
QPoint findColorButtonCenter(const QByteArray &png,
|
||||
int targetR = 255,
|
||||
int targetG = 117,
|
||||
int targetB = 40,
|
||||
int tolerance = 55,
|
||||
int yMin = 0,
|
||||
int yMax = 0);
|
||||
|
||||
} // namespace OcrUtils
|
||||
Loading…
Reference in New Issue
Block a user