Add getRawScreenDumpAsXml to capture
This commit is contained in:
parent
052f9051e1
commit
f7a187ec88
@ -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; };
|
||||
|
||||
@ -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-байты), без временного файла
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user