1
0
forked from BRT/arc

Improve package state probing and logging in AdbUtils and enhance "Other Bank" button handling in PayByPhoneScript.

This commit is contained in:
slava 2026-05-14 18:36:33 +07:00
parent ff4a468b35
commit 7e5f90a450
2 changed files with 132 additions and 21 deletions

View File

@ -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<QString, QString> 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;
}

View File

@ -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;