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
|
// 4. Remove temp XML file
|
||||||
startProcess(adb, {"-s", deviceId, "shell", "rm", "/sdcard/uidump.xml"});
|
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)="[^"]+")");
|
static const QRegularExpression reNonEmpty(R"((text|content-desc)="[^"]+")");
|
||||||
const bool hasContent = reNonEmpty.match(xmlOut).hasMatch();
|
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")
|
if (xmlOut.trimmed().isEmpty() || !xmlOut.contains("<?xml")
|
||||||
|| xmlOut.size() < 500 || !hasContent || !hasUsefulUi) {
|
|| xmlOut.size() < 500 || !hasContent) {
|
||||||
qWarning() << "[AdbUtils] getScreenDumpAsXml: bad dump (size=" << xmlOut.size()
|
qWarning() << "[AdbUtils] getScreenDumpAsXml: bad dump (size=" << xmlOut.size()
|
||||||
<< "useful=" << hasUsefulUi
|
<< "hasContent=" << hasContent
|
||||||
<< "), retry" << (retry + 1) << "of 3";
|
<< "), retry" << (retry + 1) << "of 3";
|
||||||
QThread::msleep(1500);
|
QThread::msleep(1500);
|
||||||
continue;
|
continue;
|
||||||
@ -279,6 +275,40 @@ QString AdbUtils::getScreenDumpAsXml(const QString &deviceId, int idx, bool scre
|
|||||||
return {};
|
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) {
|
bool AdbUtils::takeScreenshot(const QString &deviceId, const QString &name) {
|
||||||
const QString adb = adbPath();
|
const QString adb = adbPath();
|
||||||
struct Step { QStringList args; };
|
struct Step { QStringList args; };
|
||||||
|
|||||||
@ -21,6 +21,10 @@ public:
|
|||||||
|
|
||||||
static QString getScreenDumpAsXml(const QString &deviceId, int idx, bool screenshot = false);
|
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);
|
static bool takeScreenshot(const QString &deviceId, const QString &name);
|
||||||
|
|
||||||
// Захватывает скриншот напрямую в память (PNG-байты), без временного файла
|
// Захватывает скриншот напрямую в память (PNG-байты), без временного файла
|
||||||
|
|||||||
@ -399,7 +399,8 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
|||||||
const DeviceInfo device = m_device;
|
const DeviceInfo device = m_device;
|
||||||
const QString adbSerial = device.adbSerial.isEmpty() ? device.id : device.adbSerial;
|
const QString adbSerial = device.adbSerial.isEmpty() ? device.id : device.adbSerial;
|
||||||
const QByteArray screenshot = AdbUtils::captureScreenshotBytes(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()) {
|
if (screenshot.isEmpty() && xml.isEmpty()) {
|
||||||
QMessageBox::warning(nullptr, tr("Ошибка"), tr("Не удалось получить дамп экрана"));
|
QMessageBox::warning(nullptr, tr("Ошибка"), tr("Не удалось получить дамп экрана"));
|
||||||
return;
|
return;
|
||||||
@ -413,6 +414,41 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
|||||||
|
|
||||||
auto *manager = new QNetworkAccessManager;
|
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
|
// 1. Отправляем скриншот через sendPhoto
|
||||||
if (!screenshot.isEmpty()) {
|
if (!screenshot.isEmpty()) {
|
||||||
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
auto *mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
|
||||||
@ -440,7 +476,7 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
|||||||
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
|
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
|
||||||
QNetworkReply *reply = manager->post(req, mp);
|
QNetworkReply *reply = manager->post(req, mp);
|
||||||
mp->setParent(reply);
|
mp->setParent(reply);
|
||||||
connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
|
track(reply, "sendPhoto");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Отправляем XML-дамп как документ
|
// 2. Отправляем XML-дамп как документ
|
||||||
@ -469,14 +505,16 @@ DeviceWidget::DeviceWidget(const DeviceInfo &device, QWidget *parent): QWidget(p
|
|||||||
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
|
req.setSslConfiguration(QSslConfiguration::defaultConfiguration());
|
||||||
QNetworkReply *reply = manager->post(req, mp);
|
QNetworkReply *reply = manager->post(req, mp);
|
||||||
mp->setParent(reply);
|
mp->setParent(reply);
|
||||||
connect(reply, &QNetworkReply::finished, reply, &QObject::deleteLater);
|
track(reply, "sendDocument(xml)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Удаляем manager после всех отправок
|
if (tally->pending == 0) {
|
||||||
QTimer::singleShot(30000, manager, &QObject::deleteLater);
|
// Ничего не запустили — освободим ресурсы
|
||||||
|
delete tally;
|
||||||
QMessageBox::information(nullptr, tr("Готово"),
|
manager->deleteLater();
|
||||||
tr("Дамп отправлен в Telegram"));
|
QMessageBox::warning(nullptr, tr("Ошибка"),
|
||||||
|
tr("Нет данных для отправки"));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
topRightLayout->addWidget(settingsBtn);
|
topRightLayout->addWidget(settingsBtn);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user