1
0
forked from BRT/arc
arc/android/adb/AdbUtils.cpp

765 lines
32 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "AdbUtils.h"
#include <QProcess>
#include <qDebug>
#include <QString>
#include <QCoreApplication>
#include <QFileInfo>
#include <QRegularExpression>
#include <QThread>
#include "dump/TaskDumpRecorder.h"
AdbUtils::AdbUtils(QObject *parent) : QObject(parent) {
}
QString AdbUtils::adbPath() {
#ifdef Q_OS_WIN
QString path = QCoreApplication::applicationDirPath() + "/tools/adb/windows/adb.exe";
#else
QString path = QCoreApplication::applicationDirPath() + "/tools/adb/macos/adb";
#endif
static bool checked = false;
if (!checked) {
checked = true;
QFileInfo fi(path);
if (!fi.exists()) {
qWarning() << "[AdbUtils] adb not found at:" << path;
} else if (!fi.isExecutable()) {
qWarning() << "[AdbUtils] adb is not executable:" << path;
} else {
qDebug() << "[AdbUtils] adb path:" << path;
}
}
return path;
}
QString AdbUtils::adbDir() {
return QFileInfo(adbPath()).absolutePath();
}
bool AdbUtils::startProcess(const QString &program, const QStringList &args,
QString *stdOut, QString *stdErr) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(program, args);
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start process:" << program << args
<< "error:" << process.errorString();
if (stdErr) {
*stdErr = process.errorString();
}
return false;
}
if (!process.waitForFinished(30000) ||
process.exitStatus() != QProcess::NormalExit ||
process.exitCode() != 0) {
if (stdErr) {
*stdErr = QString::fromUtf8(process.readAllStandardError());
}
qWarning() << "[AdbUtils] Process failed:" << program << args
<< "exitCode:" << process.exitCode()
<< "stderr:" << (stdErr ? *stdErr : QString::fromUtf8(process.readAllStandardError()));
return false;
}
if (stdOut) {
*stdOut = QString::fromUtf8(process.readAllStandardOutput());
}
return true;
}
// adb shell dumpsys package ru.rshb.dbo | grep permission
// TODO доделать обработку пермишинов
// adb shell pm revoke ru.rshb.dbo android.permission.WRITE_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.READ_CONTACTS; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_COARSE_LOCATION; adb shell pm revoke ru.rshb.dbo android.permission.ACCESS_FINE_LOCATION
// adb shell cmd appops set ru.rshb.dbo POST_NOTIFICATION ignore
/**
* Возвращает список найденных устройств в виде пар (ID устройства и Статус)
* @return [(id, status), ...]
*/
QList<QPair<QString, QString> > AdbUtils::getListDevices() {
const QString adb = adbPath();
const QString dir = adbDir();
// Убиваем чужой adb-сервер (возможно другой версии) и запускаем свой,
// чтобы избежать конфликта версий (например, scrcpy adb 35 vs bundled adb 36)
static bool serverRestarted = false;
if (!serverRestarted) {
serverRestarted = true;
QProcess kill;
kill.setWorkingDirectory(dir);
kill.start(adb, {"kill-server"});
kill.waitForFinished(5000);
qDebug() << "[AdbUtils] Killed existing adb server";
QProcess start;
start.setWorkingDirectory(dir);
start.start(adb, {"start-server"});
start.waitForFinished(10000);
qDebug() << "[AdbUtils] Started own adb server";
}
QProcess process;
process.setWorkingDirectory(dir);
process.start(adb, {"devices"});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb devices:" << process.errorString();
return {};
}
process.waitForFinished(10000);
const QString output = process.readAllStandardOutput();
QList<QPair<QString, QString> > devices;
const QStringList lines = output.split('\n');
for (int i = 1; i < lines.size(); ++i) {
QString line = lines.at(i).trimmed();
if (line.isEmpty()) {
continue;
}
/**
* device — устройство готово к использованию.
* offline — устройство подключено, но не может быть использовано.
* unauthorized — устройство не авторизовано для работы с ADB (нужно подтвердить запрос на подключение на самом устройстве).
* recovery — устройство в режиме восстановления.
*/
if (QStringList parts = line.split('\t'); parts.size() == 2) {
const QString &id = parts.at(0);
const QString &status = parts.at(1);
devices.append(qMakePair(id, status));
}
}
return devices;
}
QString AdbUtils::tryToGetRunningAppPid(const QString &deviceId, const QString &packageName) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "pidof", packageName
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb pidof:" << process.errorString();
return {};
}
process.waitForFinished();
QString pid = process.readAllStandardOutput();
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(), {
"-s", deviceId,
"shell", "monkey",
"-p", packageName,
"-c", "android.intent.category.LAUNCHER",
"1"
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb monkey:" << process.errorString();
return false;
}
process.waitForFinished();
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();
// Полный вывод 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;
}
// Фолбэк: 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;
}
bool AdbUtils::tryToKillApplication(const QString &deviceId, const QString &packageName) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "am",
"force-stop", packageName
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb force-stop:" << process.errorString();
return false;
}
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
// idx - временный лог
// adb -s 6edc4a47 shell 'uiautomator dump /sdcard/uidump.xml' && adb -s 6edc4a47 pull /sdcard/uidump.xml step_1.xml && adb -s 6edc4a47 shell 'rm /sdcard/uidump.xml'
QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool screenshot) {
const QString adb = adbPath();
for (int retry = 0; retry < 3; ++retry) {
// 1. Dump UI hierarchy (retry up to 3 times — uiautomator may be killed by Android)
bool dumpOk = false;
for (int attempt = 0; attempt < 3; ++attempt) {
if (startProcess(adb, {"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"})) {
dumpOk = true;
break;
}
qWarning() << "[AdbUtils] uiautomator dump failed, attempt" << (attempt + 1) << "of 3";
QThread::msleep(1000);
}
if (!dumpOk) {
qWarning() << "Failed to dump UI hierarchy after 3 attempts.";
return {};
}
// 2. Optionally pull and save the XML file
if (screenshot) {
if (!startProcess(adb, {"-s", deviceId, "pull", "/sdcard/uidump.xml",
QString("step_%1.xml").arg(idx)})) {
qWarning() << "Failed to pull UIDump XML.";
return {};
}
}
// 3. Read the XML content
QString xmlOut;
if (!startProcess(adb, {"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}, &xmlOut)) {
qWarning() << "Failed to read UIDump XML.";
return {};
}
// 4. Remove temp XML file
startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"});
// 5. Минимальная санитарная проверка: дамп не пустой, достаточного размера
// и содержит хотя бы одну ноду с непустым text или content-desc.
// Этого хватает, чтобы отсеять WebView-shell-only состояния Ozon
// (там в шелле labeled нод вообще нет), и при этом не отбрасывать
// экраны RN/Compose (Dushanbe, Black), где интерактив строится на
// LinearLayout/View с content-desc, без Button/text/EditText.
static const QRegularExpression reNonEmpty(R"((text|content-desc)="[^"]+")");
const bool hasContent = reNonEmpty.match(xmlOut).hasMatch();
if (xmlOut.trimmed().isEmpty() || !xmlOut.contains("<?xml")
|| xmlOut.size() < 500 || !hasContent) {
qWarning() << "[AdbUtils] getScreenDumpAsXml: bad dump (size=" << xmlOut.size()
<< "hasContent=" << hasContent
<< "), retry" << (retry + 1) << "of 3";
QThread::msleep(1500);
continue;
}
// 6. Screenshot (if needed)
if (screenshot) {
startProcess(adb, {"-s", deviceId, "shell", "screencap", "-p", "/sdcard/screen.png"});
startProcess(adb, {"-s", deviceId, "pull", "/sdcard/screen.png",
QString("screen_step_%1.png").arg(idx)});
startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/screen.png"});
}
// 7. Обрезка к началу XML
QByteArray xmlBytes = xmlOut.toUtf8();
if (const int start = xmlBytes.indexOf("<?xml"); start != -1) {
xmlBytes = xmlBytes.mid(start);
}
const QString cleanXml = QString::fromUtf8(xmlBytes);
if (auto *rec = TaskDumpRecorder::current()) {
rec->saveXml(idx, cleanXml);
}
return cleanXml;
}
qWarning() << "[AdbUtils] getScreenDumpAsXml: empty dump after 3 retries";
return {};
}
QString AdbUtils::getRawScreenDumpAsXml(const QString &deviceId) {
const QString adb = adbPath();
// 1. Дампим иерархию (uiautomator может умирать — до 3 попыток)
bool dumpOk = false;
for (int attempt = 0; attempt < 3; ++attempt) {
if (startProcess(adb, {"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"})) {
dumpOk = true;
break;
}
QThread::msleep(500);
}
if (!dumpOk) {
qWarning() << "[AdbUtils] getRawScreenDumpAsXml: uiautomator dump failed";
return {};
}
// 2. Читаем
QString xmlOut;
if (!startProcess(adb, {"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}, &xmlOut)) {
qWarning() << "[AdbUtils] getRawScreenDumpAsXml: cat failed";
return {};
}
startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"});
// 3. Обрезка к началу XML; в отличие от getScreenDumpAsXml — без фильтра по контенту,
// оператору важно видеть РЕАЛЬНОЕ состояние экрана (PIN, сплэш, WebView и т.д.).
QByteArray xmlBytes = xmlOut.toUtf8();
if (const int start = xmlBytes.indexOf("<?xml"); start != -1) {
xmlBytes = xmlBytes.mid(start);
}
return QString::fromUtf8(xmlBytes);
}
bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) {
const QString adb = adbPath();
struct Step { QStringList args; };
Step steps[] = {
{{"-s", deviceId, "shell", "screencap", "-p", "/sdcard/screen.png"}},
{{"-s", deviceId, "pull", "/sdcard/screen.png", name + ".png"}},
{{"-s", deviceId, "shell", "rm", "/sdcard/screen.png"}}
};
for (const auto &step : steps) {
QString stdErr;
if (!startProcess(adb, step.args, nullptr, &stdErr)) {
qWarning() << "ADB command failed:" << step.args.join(' ');
qWarning() << "Error:" << stdErr;
return false;
}
}
return true;
}
QByteArray AdbUtils::captureScreenshotBytes(const QString &deviceId) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {"-s", deviceId, "exec-out", "screencap", "-p"});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] captureScreenshotBytes failed to start for" << deviceId
<< "error:" << process.errorString();
return {};
}
if (!process.waitForFinished(10000) || process.exitCode() != 0) {
qWarning() << "[AdbUtils] captureScreenshotBytes failed for" << deviceId;
return {};
}
return process.readAllStandardOutput();
}
bool AdbUtils::takeXmlDump(const QString &deviceId, const QString &name) {
const QString adb = adbPath();
struct Step { QStringList args; };
Step steps[] = {
{{"-s", deviceId, "shell", "uiautomator", "dump", "/sdcard/uidump.xml"}},
{{"-s", deviceId, "pull", "/sdcard/uidump.xml", name + ".xml"}},
{{"-s", deviceId, "shell", "cat", "/sdcard/uidump.xml"}},
{{"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"}}
};
for (const auto &step : steps) {
QString stdErr;
if (!startProcess(adb, step.args, nullptr, &stdErr)) {
qWarning() << "ADB command failed:" << step.args.join(' ');
qWarning() << "Error:" << stdErr;
return false;
}
}
return true;
}
bool AdbUtils::makeTap(const QString &deviceId, const int x, const int y) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "input",
"tap", QString::number(x), QString::number(y)
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb tap:" << process.errorString();
return false;
}
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::enableAdbIMEKeyboard(const QString &deviceId) {
const QString adb = adbPath();
const QString imeId = QStringLiteral("com.android.adbkeyboard/.AdbIME");
// Проверяем, установлен ли ADB Keyboard
QString installedList;
startProcess(adb, {"-s", deviceId, "shell", "pm", "list", "packages", "com.android.adbkeyboard"}, &installedList);
bool freshlyInstalled = false;
if (!installedList.contains("com.android.adbkeyboard")) {
const QString apkPath = QCoreApplication::applicationDirPath() + "/assets/ADBKeyboard.apk";
qDebug() << "[AdbUtils] ADB Keyboard not installed, installing from:" << apkPath;
QString installErr;
if (!startProcess(adb, {"-s", deviceId, "install", apkPath}, nullptr, &installErr)) {
qWarning() << "[AdbUtils] Failed to install ADB Keyboard:" << installErr;
return false;
}
qDebug() << "[AdbUtils] ADB Keyboard installed successfully";
freshlyInstalled = true;
}
// Пакет мог быть отключён производителем (pm disable-user).
// Пытаемся включить; ошибку не считаем фатальной (на некоторых прошивках команда
// отвергается, но сам IME при этом включается).
{
QString pmOut, pmErr;
if (!startProcess(adb, {"-s", deviceId, "shell", "pm", "enable", "com.android.adbkeyboard"}, &pmOut, &pmErr)) {
qWarning() << "[AdbUtils] pm enable com.android.adbkeyboard failed:" << pmErr.trimmed()
<< "(продолжаем)";
}
}
// ime enable с retry: сразу после 'pm install' InputMethodManagerService
// может ещё не подхватить новый IME и возвращать "Unknown input method ... cannot be enabled".
// Ретраим до появления IME в списке всех зарегистрированных (ime list -a).
const int enableAttempts = freshlyInstalled ? 10 : 3;
bool imeEnabled = false;
QString lastErr;
for (int attempt = 0; attempt < enableAttempts; ++attempt) {
QString out, err;
if (startProcess(adb, {"-s", deviceId, "shell", "ime", "enable", imeId}, &out, &err)) {
qDebug() << "[AdbUtils] ime enable:" << out.trimmed();
imeEnabled = true;
break;
}
lastErr = err.trimmed();
qDebug() << "[AdbUtils] ime enable attempt" << attempt + 1 << "/" << enableAttempts
<< "failed:" << lastErr << "— ждём, пока IMMS перечитает IME";
QThread::msleep(1000);
}
if (!imeEnabled) {
qWarning() << "[AdbUtils] ime enable failed after" << enableAttempts
<< "attempts. Last error:" << lastErr;
return false;
}
// Верификация: IME должен появиться в списке разрешённых
{
QString enabledList;
startProcess(adb, {"-s", deviceId, "shell", "ime", "list", "-s"}, &enabledList);
if (!enabledList.contains(imeId)) {
qWarning() << "[AdbUtils] ADB Keyboard отсутствует в 'ime list -s' после 'ime enable'."
<< "Вывод:" << enabledList.trimmed()
<< "(прошивка/OEM может блокировать сторонние IME)";
return false;
}
}
// ime set
{
QString out, err;
if (!startProcess(adb, {"-s", deviceId, "shell", "ime", "set", imeId}, &out, &err)) {
qWarning() << "[AdbUtils] ime set failed:" << err.trimmed();
return false;
}
qDebug() << "[AdbUtils] ime set:" << out.trimmed();
}
// Верификация: активный IME должен совпасть с нашим
{
QString defaultIme;
startProcess(adb, {"-s", deviceId, "shell", "settings", "get", "secure", "default_input_method"}, &defaultIme);
const QString trimmed = defaultIme.trimmed();
if (!trimmed.contains(imeId)) {
qWarning() << "[AdbUtils] default_input_method =" << trimmed
<< "— ожидали" << imeId
<< "(прошивка откатывает смену IME)";
return false;
}
qDebug() << "[AdbUtils] Active IME verified:" << trimmed;
}
return true;
}
bool AdbUtils::inputAdbIMEText(const QString &deviceId, const QString &text) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "am broadcast -a ADB_INPUT_TEXT --es msg '" + text + "'"
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb broadcast:" << process.errorString();
return false;
}
process.waitForFinished();
qDebug() << "[AdbUtils] inputAdbIMEText result:" << process.exitCode()
<< "output:" << process.readAllStandardOutput().trimmed()
<< "error:" << process.readAllStandardError().trimmed();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::disableAdbIMEKeyboard(const QString &deviceId) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {"-s", deviceId, "shell", "ime", "disable",
"com.android.adbkeyboard/.AdbIME"});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb ime disable:" << process.errorString();
return false;
}
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::makeSwipe(const QString &deviceId, const int x1, const int y1, const int x2, const int y2) {
return makeSwipe(deviceId, x1, y1, x2, y2, -1);
}
bool AdbUtils::makeSwipe(const QString &deviceId, const int x1, const int y1, const int x2, const int y2, int durationMs) {
QStringList args = {
"-s", deviceId,
"shell", "input",
"swipe",
QString::number(x1), QString::number(y1),
QString::number(x2), QString::number(y2)
};
if (durationMs > 0) args << QString::number(durationMs);
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), args);
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb swipe:" << process.errorString();
return false;
}
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::inputText(const QString &deviceId, const QString &text) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "input",
"text", text
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb input text:" << process.errorString();
return false;
}
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
bool AdbUtils::hideKeyboard(const QString &deviceId) {
// Метод 1: KEYCODE_ESCAPE (111) — работает на большинстве Android-устройств,
// но игнорируется на некоторых прошивках (например, TECNO/HiOS).
{
QProcess p;
p.setWorkingDirectory(adbDir());
p.start(adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "111"});
if (p.waitForStarted(5000)) p.waitForFinished(3000);
}
// Метод 2: KEYCODE_BACK (4) — Android посылает BACK сначала в IME; если
// IME (клавиатура) видим — он его потребляет и закрывает, не уходя
// на предыдущий экран. На TECNO/HiOS это часто единственный рабочий
// вариант. Если клавиатура уже скрыта (после метода 1), BACK уйдёт
// в Activity — нежелательно. Поэтому сначала проверим через dumpsys
// input_method, видна ли всё ещё клавиатура.
bool imeVisible = false;
{
QProcess p;
p.setWorkingDirectory(adbDir());
p.start(adbPath(), {"-s", deviceId, "shell", "dumpsys", "input_method"});
if (p.waitForStarted(5000) && p.waitForFinished(5000)) {
const QString out = QString::fromUtf8(p.readAllStandardOutput());
imeVisible = out.contains(QStringLiteral("mInputShown=true"))
|| out.contains(QStringLiteral("mServedInputConnection=")) // fallback heuristic
&& !out.contains(QStringLiteral("mInputShown=false"));
}
}
if (imeVisible) {
qDebug() << "[AdbUtils] hideKeyboard: ESC didn't dismiss IME, trying BACK";
QProcess p;
p.setWorkingDirectory(adbDir());
p.start(adbPath(), {"-s", deviceId, "shell", "input", "keyevent", "4"});
if (p.waitForStarted(5000)) p.waitForFinished(3000);
}
return true;
}
bool AdbUtils::goBack(const QString &deviceId) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "input",
"keyevent", "4"
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb keyevent:" << process.errorString();
return false;
}
process.waitForFinished();
return process.exitStatus() == QProcess::NormalExit && process.exitCode() == 0;
}
QSet<QString> AdbUtils::getListApps(const QString &deviceId) {
QProcess process;
process.setWorkingDirectory(adbDir());
process.start(adbPath(), {
"-s", deviceId,
"shell", "pm list",
"packages", "-3"
});
if (!process.waitForStarted(5000)) {
qWarning() << "[AdbUtils] Failed to start adb pm list:" << process.errorString();
return {};
}
process.waitForFinished();
const QString output = process.readAllStandardOutput();
QSet<QString> apps;
const QStringList lines = output.split("\n");
for (int i = 0; i < lines.size(); ++i) {
QString line = lines.at(i).trimmed().replace("package:", "");
if (line.isEmpty()) {
continue;
}
apps.insert(line);
}
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();
// Проверяем, включён ли экран (mWakefulness=Awake)
QString powerState;
startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "power"}, &powerState);
const bool isAwake = powerState.contains("mWakefulness=Awake");
if (!isAwake) {
// Включаем экран
startProcess(adb, {"-s", deviceId, "shell", "input", "keyevent", "KEYCODE_WAKEUP"});
QThread::msleep(500);
qDebug() << "[AdbUtils] unlockScreen: woke up device" << deviceId;
}
// Проверяем, залочен ли экран (mDreamingSleepToken / mShowingLockscreen)
QString wmState;
startProcess(adb, {"-s", deviceId, "shell", "dumpsys", "window"}, &wmState);
const bool isLocked = wmState.contains("mDreamingSleepToken") || wmState.contains("mShowingLockscreen=true")
|| wmState.contains("StatusBar");
if (isLocked) {
// Свайп вверх для разблокировки
const int x = screenWidth / 2;
makeSwipe(deviceId, x, screenHeight * 3 / 4, x, screenHeight / 4);
QThread::msleep(500);
qDebug() << "[AdbUtils] unlockScreen: swiped to unlock" << deviceId;
}
}