1
0
forked from BRT/arc

Add getRawScreenDumpAsXml to capture

This commit is contained in:
slava 2026-04-27 20:41:16 +07:00
parent 052f9051e1
commit f7a187ec88
3 changed files with 92 additions and 20 deletions

View File

@ -234,22 +234,18 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre
// 4. Remove temp XML file
startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"});
// 5. Проверяем что XML не пустой, достаточного размера и содержит непустые UI-элементы
// 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();
// Дополнительно: ловим "WebView-shell-only" состояние, когда AT отдаёт
// только обёртку WebView и нижнюю нав-панель Ozon (FINANCE_TAB*),
// но Svelte-контент ещё не отрисован. Признак полезного среза —
// хотя бы один Button с непустым text ИЛИ EditText. Нав-табы у Ozon
// имеют пустой text (только content-desc), поэтому регекс их не зачтёт.
static const QRegularExpression reUsefulUi(
R"(class="android\.widget\.Button"[^/]*text="[^"]+"|text="[^"]+"[^/]*class="android\.widget\.Button"|class="android\.widget\.EditText")"
);
const bool hasUsefulUi = reUsefulUi.match(xmlOut).hasMatch();
if (xmlOut.trimmed().isEmpty() || !xmlOut.contains("<?xml")
|| xmlOut.size() < 500 || !hasContent || !hasUsefulUi) {
|| xmlOut.size() < 500 || !hasContent) {
qWarning() << "[AdbUtils] getScreenDumpAsXml: bad dump (size=" << xmlOut.size()
<< "useful=" << hasUsefulUi
<< "hasContent=" << hasContent
<< "), retry" << (retry + 1) << "of 3";
QThread::msleep(1500);
continue;
@ -279,6 +275,40 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre
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; };

View File

@ -21,6 +21,10 @@ public:
static QString getScreenDumpAsXml(const QString &deviceId, int idx, bool screenshot = false);
// Сырой дамп без фильтра "useful UI" — для ручного превью оператора:
// отдаёт что есть на экране, даже если это PIN-пад / сплэш / WebView-shell.
static QString getRawScreenDumpAsXml(const QString &deviceId);
static bool takeScreenshot(const QString &deviceId, const QString &name);
// Захватывает скриншот напрямую в память (PNG-байты), без временного файла

View File

@ -399,7 +399,8 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
const DeviceInfo device = m_device;
const QString adbSerial = device.adbSerial.isEmpty() ? device.id : device.adbSerial;
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(adbSerial);
const QString xml = AdbUtils::getScreenDumpAsXml(adbSerial, 0);
// Сырой дамп (без фильтра "useful UI"): нужен любой экран — PIN, сплэш, WebView, меню.
const QString xml = AdbUtils::getRawScreenDumpAsXml(adbSerial);
if (screenshot.isEmpty() && xml.isEmpty()) {
QMessageBox::warning(nullptr, tr("Ошибка"), tr("Не удалось получить дамп экрана"));
return;
@ -413,6 +414,41 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
auto *manager = new QNetworkAccessManager;
struct Tally {
int pending = 0;
QStringList errors;
};
auto *tally = new Tally;
auto track = [manager, tally](QNetworkReply *reply, const QString &label) {
++tally->pending;
connect(reply, &QNetworkReply::finished, reply, [reply, manager, tally, label]() {
if (reply->error() != QNetworkReply::NoError) {
tally->errors << QString("%1: %2").arg(label, reply->errorString());
} else {
// Telegram возвращает 200 + JSON {"ok":false,...} даже на ошибки уровня API
const QByteArray body = reply->readAll();
if (!body.contains("\"ok\":true")) {
tally->errors << QString("%1: API rejected (%2)")
.arg(label, QString::fromUtf8(body.left(200)));
}
}
reply->deleteLater();
if (--tally->pending == 0) {
if (tally->errors.isEmpty()) {
QMessageBox::information(nullptr, QObject::tr("Готово"),
QObject::tr("Дамп отправлен в Telegram"));
} else {
QMessageBox::warning(nullptr, QObject::tr("Ошибка отправки"),
QObject::tr("Не удалось отправить часть данных:\n%1")
.arg(tally->errors.join("\n")));
}
delete tally;
manager->deleteLater();
}
});
};
// 1. Отправляем скриншот через sendPhoto
if (!screenshot.isEmpty()) {
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
@ -440,7 +476,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
QNetworkReply *reply = manager->post(req, mp);
mp->setParent(reply);
connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
track(reply, "sendPhoto");
}
// 2. Отправляем XML-дамп как документ
@ -469,14 +505,16 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
QNetworkReply *reply = manager->post(req, mp);
mp->setParent(reply);
connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
track(reply, "sendDocument(xml)");
}
// Удаляем manager после всех отправок
QTimer::singleShot(30000, manager, &QObject::deleteLater);
QMessageBox::information(nullptr, tr("Готово"),
tr("Дамп отправлен в Telegram"));
if (tally->pending == 0) {
// Ничего не запустили — освободим ресурсы
delete tally;
manager->deleteLater();
QMessageBox::warning(nullptr, tr("Ошибка"),
tr("Нет данных для отправки"));
}
});
topRightLayout->addWidget(settingsBtn);