diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index 45b7283..fa7b228 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -152,7 +152,71 @@ QString AdbUtils::tryToGetRunningAppPid(const QString &deviceId, const QString & return pid.replace("\n", ""); } +// Логирует длинный вывод построчно с префиксом, иначе Qt-логгер режет строку. +static void logMultiline(const char *tag, const QString &text) { + if (text.isEmpty()) { + qDebug().noquote() << tag << "(empty)"; + return; + } + const QStringList lines = text.split(QRegularExpression("\r?\n")); + for (const QString &line : lines) { + if (!line.isEmpty()) qDebug().noquote() << tag << line; + } +} + +// Прогоняет adb-команду и возвращает stdout (stderr игнорируется — этим функциям важен только stdout). +static QString adbShellStdout(const QStringList &args, int timeoutMs = 10000) { + QProcess p; + p.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath()); + p.start(AdbUtils::adbPath(), args); + if (!p.waitForStarted(5000)) return {}; + p.waitForFinished(timeoutMs); + return QString::fromUtf8(p.readAllStandardOutput()); +} + +// Возвращает {launcherActivity, summary}. launcherActivity вида "pkg/.MainActivity" или пустая строка. +static QPair probePackageState(const QString &deviceId, const QString &packageName) { + const QString resolveOut = adbShellStdout( + {"-s", deviceId, "shell", "cmd", "package", "resolve-activity", "--brief", packageName}); + QString launcher; + // Формат: первая строка — приоритет, вторая — "pkg/activity". + for (const QString &raw : resolveOut.split(QRegularExpression("\r?\n"))) { + const QString line = raw.trimmed(); + if (line.contains('/') && line.startsWith(packageName + "/")) { + launcher = line; + break; + } + } + + const QString dump = adbShellStdout( + {"-s", deviceId, "shell", "dumpsys", "package", packageName}, 15000); + + auto pickFlag = [&dump](const QString &key) -> QString { + const QRegularExpression re(key + "=(\\S+)"); + const auto m = re.match(dump); + return m.hasMatch() ? m.captured(1) : QStringLiteral("?"); + }; + const bool installed = dump.contains("Package [" + packageName + "]"); + const QString enabled = pickFlag("enabled"); + const QString stopped = pickFlag("stopped"); + const QString hidden = pickFlag("hidden"); + const bool hasLauncherCategory = dump.contains("android.intent.category.LAUNCHER"); + + const QString summary = QString( + "installed=%1 enabled=%2 stopped=%3 hidden=%4 hasLauncherCategory=%5 resolveActivity=%6") + .arg(installed ? "true" : "false", enabled, stopped, hidden, + hasLauncherCategory ? "true" : "false", + launcher.isEmpty() ? "(none)" : launcher); + return {launcher, summary}; +} + bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &packageName) { + // Pre-flight: состояние пакета. Без этого по логу нельзя отличить + // "нет иконки" от "disabled" от "hidden" от "не установлен". + const auto [launcherActivity, stateSummary] = probePackageState(deviceId, packageName); + qDebug().noquote() << "[AdbUtils::tryToStartApplication] device=" << deviceId + << "pkg=" << packageName << "state:" << stateSummary; + QProcess process; process.setWorkingDirectory(adbDir()); process.start(adbPath(), { @@ -167,16 +231,47 @@ bool AdbUtils::tryToStartApplication(const QString &deviceId, const QString &pac return false; } process.waitForFinished(); - const QString output = process.readAllStandardOutput(); + const QString stdOut = QString::fromUtf8(process.readAllStandardOutput()); + const QString stdErr = QString::fromUtf8(process.readAllStandardError()); + const int exitCode = process.exitCode(); + const QProcess::ExitStatus exitStatus = process.exitStatus(); - // Если процесс запустился, там будет такая строка. - // Если нет, то: "** No activities found to run, monkey aborted." - if (output.contains("Events injected: 1")) { + // Полный вывод monkey пишем построчно — Qt-логгер режет длинные QString, + // из-за чего хвост (`Events injected` / `No activities found`) терялся. + qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey exitStatus=" + << exitStatus << "exitCode=" << exitCode; + logMultiline("[AdbUtils::tryToStartApplication] monkey stdout:", stdOut); + logMultiline("[AdbUtils::tryToStartApplication] monkey stderr:", stdErr); + + if (stdOut.contains("Events injected: 1")) { return true; } - // Тут надо в логи писать, почему не стартует - qDebug() << "Start app output:" << output; + // Фолбэк: am start с конкретной activity. У него осмысленный stderr и exit code, + // в отличие от monkey, который при пустом resolve просто молча печатает "No activities found". + if (!launcherActivity.isEmpty()) { + qDebug().noquote() << "[AdbUtils::tryToStartApplication] monkey не дал Events injected;" + << "пробую am start -n" << launcherActivity; + QProcess am; + am.setWorkingDirectory(adbDir()); + am.start(adbPath(), + {"-s", deviceId, "shell", "am", "start", "-W", "-n", launcherActivity}); + am.waitForStarted(5000); + am.waitForFinished(15000); + const QString amOut = QString::fromUtf8(am.readAllStandardOutput()); + const QString amErr = QString::fromUtf8(am.readAllStandardError()); + qDebug().noquote() << "[AdbUtils::tryToStartApplication] am exitCode=" << am.exitCode(); + logMultiline("[AdbUtils::tryToStartApplication] am stdout:", amOut); + logMultiline("[AdbUtils::tryToStartApplication] am stderr:", amErr); + // `am start -W` печатает "Status: ok" при успехе. + if (am.exitCode() == 0 && amOut.contains("Status: ok")) { + return true; + } + } else { + qWarning() << "[AdbUtils::tryToStartApplication] нет launcher-активити для" << packageName + << "— приложение либо не установлено, либо disabled/hidden, либо без LAUNCHER intent-filter"; + } + return false; } diff --git a/android/ozon/PayByPhoneScript.cpp b/android/ozon/PayByPhoneScript.cpp index 854dd5a..e005169 100644 --- a/android/ozon/PayByPhoneScript.cpp +++ b/android/ozon/PayByPhoneScript.cpp @@ -296,23 +296,39 @@ void PayByPhoneScript::makePayment( // Экран выбора банка: ищем "Другой банк" (текст) или EditText с hint "Название банка" if (!bankSelected) { - // Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой) - bool foundOtherBank = false; - { - Node node; + // Вариант 1: кнопка "Другой банк" (content-desc="other-bank-item", text пустой). + // На устройствах с маленьким экраном кнопка лежит в конце виртуализированного + // списка банков и приходит с bounds=[0,0][0,0] — Node::isEmpty это не отлавливает + // (требует все координаты < 0), и тап уходит в (0,0). Скроллим список вниз, + // пока bounds не станут реальными, и только тогда тапаем. + auto findOtherBank = [&]() -> Node { for (const Node &n : xmlScreenParser.findAllNodes()) { - if (n.contentDesc.contains("other-bank-item")) { - node = n; - break; - } - } - if (!node.isEmpty()) { - qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item)..."; - AdbUtils::makeTap(deviceId, node.x(), node.y()); - QThread::msleep(2000); - foundOtherBank = true; - otherBankClicked = true; + if (n.contentDesc.contains("other-bank-item")) return n; } + return {}; + }; + + bool foundOtherBank = false; + Node node = findOtherBank(); + for (int scrollAttempt = 0; + !node.isEmpty() && (node.width() == 0 || node.height() == 0) && scrollAttempt < 6; + ++scrollAttempt) { + qDebug() << "[PayByPhone] 'Другой банк' has collapsed bounds, scrolling list down, attempt" + << scrollAttempt + 1 << "/ 6"; + AdbUtils::makeSwipe(deviceId, width / 2, height * 3 / 4, width / 2, height / 4); + QThread::msleep(800); + xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(deviceId, ++m_counter)); + closeGooglePlayBannerIfVisible(deviceId, width, height); + node = findOtherBank(); + } + + if (!node.isEmpty() && node.width() > 0 && node.height() > 0) { + qDebug() << "[PayByPhone] Tapping 'Другой банк' (other-bank-item) at" + << node.x() << "," << node.y(); + AdbUtils::makeTap(deviceId, node.x(), node.y()); + QThread::msleep(2000); + foundOtherBank = true; + otherBankClicked = true; } if (foundOtherBank) continue;