diff --git a/android/ozon/GetCardInfoScript.cpp b/android/ozon/GetCardInfoScript.cpp index b556876..fc4a1fd 100644 --- a/android/ozon/GetCardInfoScript.cpp +++ b/android/ozon/GetCardInfoScript.cpp @@ -285,13 +285,50 @@ void GetCardInfoScript::doStart() { continue; } - AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + // Открываем экран нужной карты. + if (xmlScreenParser.versionTrack() >= AppVersionTrack::V19_19_1) { + // Новый UI: тап по НИЖНЕЙ (чиповой) части тайла "Основной счёт" открывает экран + // счёта с РАЗДЕЛЬНЫМИ кнопками карт ("Bank Card NNNN"). Тапаем кнопку нужной + // карты — открывается именно она. (Тап в центр тайла открывал бы переднюю карту + // карусели, и для всех материалов читался номер одной карты.) + const int accY = cardBtn.y2() - cardBtn.height() / 5; // зона чипов карт внизу тайла + qDebug() << "[GetCardInfo] Opening account screen via tile bottom at" << cardBtn.x() << accY; + AdbUtils::makeTap(m_deviceId, cardBtn.x(), accY); - // Скроллим WebView вверх, пока "Показать" не окажется в верхней трети экрана. - // На устройствах с короткой высотой (например, SM-A165F, h=2340) кнопка "Показать" - // находится за bottom navigation, и тап уходит в нав-бар. - const int targetTopY = device.screenHeight / 3; - for (int scrollAttempt = 0; scrollAttempt < 6; ++scrollAttempt) { + Node cardTile; + for (int a = 0; a < 8; ++a) { + QThread::msleep(a == 0 ? 1500 : 700); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + cardTile = xmlScreenParser.findAccountCardButton(card.lastNumbers); + if (!cardTile.isEmpty()) break; + qDebug() << "[GetCardInfo] Waiting account card button" << card.lastNumbers + << "attempt" << a + 1 << "/ 8"; + } + if (!cardTile.isEmpty()) { + qDebug() << "[GetCardInfo] Tapping account card button" << card.lastNumbers + << "at" << cardTile.x() << cardTile.y(); + AdbUtils::makeTap(m_deviceId, cardTile.x(), cardTile.y()); + } else { + qWarning() << "[GetCardInfo] Account card button not found for" << card.lastNumbers + << "— fallback to tile center tap"; + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + } + } else { + // Старый UI: тап по тайлу открывает карту напрямую. + AdbUtils::makeTap(m_deviceId, cardBtn.x(), cardBtn.y()); + } + + // Подводим "Показать" в зону тапа [minTapY, midY]: ниже статус-бара/тулбара и + // ВЫШЕ середины экрана (после раскрытия номер/срок/CVV появляются ниже кнопки — + // ей нужно место снизу). Свайпы делаем МЕДЛЕННЫМИ (durationMs), иначе быстрый + // ф_линг перелистывает "Показать" под самый верх (y≈3, под статус-бар) и тап мимо. + // - cy > midY → кнопка ниже середины (или за нижней навигацией): листаем вверх. + // - cy < minTapY → перелистнули под статус-бар: листаем обратно вниз. + const int minTapY = device.screenHeight / 8; + const int midY = device.screenHeight / 2; + const int swipeMs = 700; + for (int scrollAttempt = 0; scrollAttempt < 8; ++scrollAttempt) { QThread::msleep(scrollAttempt == 0 ? 2000 : 600); xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); @@ -307,15 +344,24 @@ void GetCardInfoScript::doStart() { } continue; } - if (candidate.y() <= targetTopY) { - qDebug() << "[GetCardInfo] 'Показать' already at top y=" << candidate.y(); + const int cy = candidate.y(); + if (cy >= minTapY && cy <= midY) { + qDebug() << "[GetCardInfo] 'Показать' in tap zone (above middle) y=" << cy; break; } - const int swipeFromY = candidate.y(); - const int swipeToY = device.screenHeight / 6; - qDebug() << "[GetCardInfo] Scrolling 'Показать' up:" << swipeFromY << "->" << swipeToY; - AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, swipeFromY, - device.screenWidth / 2, swipeToY); + if (cy > midY) { + // Ниже середины — листаем вверх, целясь в верхнюю треть (медленно, без перелёта). + const int swipeToY = device.screenHeight / 3; + qDebug() << "[GetCardInfo] Scrolling 'Показать' up:" << cy << "->" << swipeToY; + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, cy, + device.screenWidth / 2, swipeToY, swipeMs); + } else { + // Перелистнули под статус-бар (y≈3) — листаем обратно вниз в верхнюю треть. + const int swipeToY = device.screenHeight / 3; + qDebug() << "[GetCardInfo] 'Показать' overshot to y=" << cy << ", scrolling down to" << swipeToY; + AdbUtils::makeSwipe(m_deviceId, device.screenWidth / 2, device.screenHeight / 8, + device.screenWidth / 2, swipeToY, swipeMs); + } } // Ждём экран карты и стабильной геометрии блока реквизитов. @@ -422,7 +468,16 @@ void GetCardInfoScript::doStart() { } } - if (!cardNumber.isEmpty()) { + if (!cardNumber.isEmpty() && !cardNumber.endsWith(card.lastNumbers)) { + // Прочитан номер ЧУЖОЙ карты (карусель отдала не ту) — НЕ сохраняем, иначе + // материал получит неверный номер. Раньше из-за этого все карты получали + // номер передней карты карусели. + const QString err = "Card number " + cardNumber + " does not end with target " + + card.lastNumbers + " — skipping save"; + qWarning() << "[GetCardInfo]" << err; + AppLogger::log("ozon/GetCardInfo", m_deviceId, err, + AdbUtils::captureScreenshotBytes(m_deviceId), "FETCH_PROFILE"); + } else if (!cardNumber.isEmpty()) { qDebug() << "[GetCardInfo] Card" << card.lastNumbers << "full number:" << cardNumber; // Апсёртим запись только теперь — с полным номером карты. MaterialInfo toSave = card; @@ -443,12 +498,16 @@ void GetCardInfoScript::doStart() { "FETCH_PROFILE"); } - // Возвращаемся на домашний экран для следующей карты + // Возвращаемся на домашний экран для следующей карты. В новом UI путь длиннее + // (карта → "Основной счёт" → главная), поэтому жмём "назад" до главной (макс 3). if (i < cards.size() - 1) { - AdbUtils::goBack(m_deviceId); - QThread::msleep(2000); - xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); - closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + for (int b = 0; b < 3; ++b) { + AdbUtils::goBack(m_deviceId); + QThread::msleep(1500); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight); + if (xmlScreenParser.isHomeScreen()) break; + } } } diff --git a/android/ozon/ScreenXmlParser.cpp b/android/ozon/ScreenXmlParser.cpp index 57088b9..14257af 100644 --- a/android/ozon/ScreenXmlParser.cpp +++ b/android/ozon/ScreenXmlParser.cpp @@ -454,7 +454,13 @@ Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) { for (const Node &node : m_nodes) { if (node.className != "android.widget.Button" || !node.clickable) continue; const QString t = node.text.trimmed(); - if (!t.endsWith(lastNumbers)) continue; + // Старый UI: отдельная кнопка карты оканчивается на 4 цифры. + // Compose-UI (>= 19.x): обе карты в одной плитке "… Bank Card 7919 Bank Card 9865", + // поэтому endsWith находит только последнюю — ищем ещё "Bank Card NNNN"/"Карта NNNN". + const bool matches = t.endsWith(lastNumbers) + || t.contains("Bank Card " + lastNumbers) + || t.contains(QString::fromUtf8("Карта ") + lastNumbers); + if (!matches) continue; // Отсекаем строки из ленты переводов (см. parseCardsOnHomeScreen) if (t.contains(QString::fromUtf8("Переводы")) || t.contains(QStringLiteral("**")) @@ -476,6 +482,40 @@ Node ScreenXmlParser::findCardButtonByLastNumbers(const QString &lastNumbers) { return {}; } +QString ScreenXmlParser::parseActiveCardLast4() { + // Маска реквизитов активной карты: "•••• •••• •••• 9865". Отличаем от плиток + // карусели ("…•• 9865") по наличию группы из 4 точек "••••". + static const QRegularExpression tail(R"((\d{4})\s*$)"); + for (const Node &node : m_nodes) { + const QString t = node.text.trimmed(); + if (!t.contains(QString::fromUtf8("••••"))) continue; + if (const auto m = tail.match(t); m.hasMatch()) + return m.captured(1); + } + return {}; +} + +Node ScreenXmlParser::findCardCarouselTile(const QString &lastNumbers) { + // Плитка карты на экране "Карта": Button "Bank Card Виртуальная карта •• NNNN". + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button" || !node.clickable) continue; + const QString t = node.text.trimmed(); + if (t.contains(QString::fromUtf8("Виртуальная карта")) && t.endsWith(lastNumbers)) + return node; + } + return {}; +} + +Node ScreenXmlParser::findAccountCardButton(const QString &lastNumbers) { + const QString needle = "Bank Card " + lastNumbers; + for (const Node &node : m_nodes) { + if (!node.clickable) continue; + if (node.text.trimmed() == needle || node.contentDesc.trimmed() == needle) + return node; + } + return {}; +} + Node ScreenXmlParser::findMainAccountButton() { for (const Node &node : m_nodes) { if (node.className != "android.widget.Button") continue; @@ -549,34 +589,63 @@ QList ScreenXmlParser::parseCardsOnHomeScreen() { if (commonAmount == 0.0) qWarning() << "[parseCards] Balance button not found or amount=0"; - // 2. Собираем все кнопки карт (текст заканчивается на 4 цифры). - // Записи из ленты переводов выглядят как кнопки и тоже заканчиваются на 4 цифры - // ("Евгений Дмитриевич Р. − 100 ₽ Переводы • Карта **1092") — отсеиваем по маркерам. - QList cards; - for (const Node &node : m_nodes) { - if (node.className != "android.widget.Button" || !node.clickable) continue; - QString t = node.text.trimmed(); - if (!endsWithFourDigits.match(t).hasMatch()) continue; - - // Маркеры записи из ленты переводов: - // - слово "Переводы" - // - маска "**" перед цифрами - // - знак суммы транзакции (минус U+2212 или плюс) - if (t.contains(QString::fromUtf8("Переводы")) + // Маркеры записи из ленты переводов (не карта): + // - слово "Переводы", маска "**" перед цифрами, знак суммы (минус U+2212 или плюс). + auto isTransferFeedRow = [](const QString &t) { + return t.contains(QString::fromUtf8("Переводы")) || t.contains(QStringLiteral("**")) - || t.contains(QChar(0x2212)) || t.contains(QChar('+'))) { + || t.contains(QChar(0x2212)) || t.contains(QChar('+')); + }; + + // 2. Собираем номера карт. + // В Compose-UI Ozon (>= 19.x) ОБЕ карты лежат в тексте балансовой кнопки одной + // строкой: "Основной счёт … ₽ Bank Card 7919 Bank Card 9865". Вытаскиваем ВСЕ + // вхождения "Bank Card NNNN" / "Карта NNNN" из текста кнопок (а не только t.right(4), + // который терял все карты кроме последней). + QList cards; + QStringList seen; + static const QRegularExpression bankCardRx(R"((?:Bank Card|Карта)\s+(\d{4}))"); + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button") continue; + const QString t = node.text; + if (isTransferFeedRow(t)) { qDebug() << "[parseCards] Skipping transfer-feed row:" << t; continue; } + auto it = bankCardRx.globalMatch(t); + while (it.hasNext()) { + const QString ln = it.next().captured(1); + if (seen.contains(ln)) continue; + seen.append(ln); + MaterialInfo acc; + acc.lastNumbers = ln; + acc.amount = commonAmount; + acc.currency = "RUB"; + acc.description = "Виртуальная карта"; + cards.append(acc); + qDebug() << "[parseCards] Card found (Bank Card):" << ln << "from:" << t; + } + } - MaterialInfo acc; - acc.lastNumbers = t.right(4); - acc.amount = commonAmount; - acc.currency = "RUB"; - acc.description = "Виртуальная карта"; - qDebug() << "[parseCards] Card found: lastNumbers=" << acc.lastNumbers - << "text=" << node.text << "amount=" << commonAmount; - cards.append(acc); + // 3. Fallback (старый UI): отдельные кнопки, текст которых оканчивается 4 цифрами. + if (cards.isEmpty()) { + for (const Node &node : m_nodes) { + if (node.className != "android.widget.Button" || !node.clickable) continue; + const QString t = node.text.trimmed(); + if (!endsWithFourDigits.match(t).hasMatch()) continue; + if (isTransferFeedRow(t)) { + qDebug() << "[parseCards] Skipping transfer-feed row:" << t; + continue; + } + MaterialInfo acc; + acc.lastNumbers = t.right(4); + acc.amount = commonAmount; + acc.currency = "RUB"; + acc.description = "Виртуальная карта"; + qDebug() << "[parseCards] Card found (fallback):" << acc.lastNumbers + << "text=" << node.text << "amount=" << commonAmount; + cards.append(acc); + } } qDebug() << "[parseCards] Total cards found:" << cards.size(); diff --git a/android/ozon/ScreenXmlParser.h b/android/ozon/ScreenXmlParser.h index 1ef296e..ca49400 100644 --- a/android/ozon/ScreenXmlParser.h +++ b/android/ozon/ScreenXmlParser.h @@ -73,6 +73,19 @@ public: // Finds a card button whose text ends with the given last 4 digits Node findCardButtonByLastNumbers(const QString &lastNumbers); + // Экран "Карта": last4 активной карты из маски "•••• •••• •••• NNNN". + // Пусто, если маскированный номер не найден. + QString parseActiveCardLast4(); + + // Экран "Карта": плитка карты в карусели по последним 4 цифрам + // ("Bank Card Виртуальная карта •• NNNN"). Тап по ней делает карту активной. + Node findCardCarouselTile(const QString &lastNumbers); + + // Экран "Основной счёт": отдельная кнопка карты "Bank Card NNNN" (text или + // content-desc). Тап по ней открывает экран именно этой карты (а не передней + // карты карусели). NNNN — последние 4 цифры. + Node findAccountCardButton(const QString &lastNumbers); + // Returns all cards visible on the home screen as MaterialInfo stubs // (lastNumbers, description, amount filled; id=-1, cardNumber empty) QList parseCardsOnHomeScreen();