1
0
forked from BRT/arc
arc/services/DeviceScreener.cpp

729 lines
34 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 "DeviceScreener.h"
#include <QSettings>
#include <QProcess>
#include <QFile>
#include <QFileInfo>
#include <QBuffer>
#include <qeventloop.h>
#include <QThread>
#include <QThreadPool>
#include <QtConcurrent>
#include <QTimer>
#include <QImage>
#include <QCryptographicHash>
#include <QHttpMultiPart>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <dao/DeviceDAO.h>
#include <db/DeviceInfo.h>
#include "adb/AdbUtils.h"
#include "dao/BankProfileDAO.h"
#include "dao/MaterialDAO.h"
#include "dao/SettingsDAO.h"
#include "net/NetworkErrorUtils.h"
#include "net/NetworkService.h"
DeviceScreener::DeviceScreener(QObject *parent) : QObject(parent) {
const QSettings settings("config.ini", QSettings::IniFormat);
m_urlAppUpdate = settings.value("net/appInfoUpdate").toString();
m_token = settings.value("common/token").toString();
m_apiBase = settings.value("net/apiBase").toString();
}
DeviceScreener::~DeviceScreener() = default;
void DeviceScreener::postDevicesData(const QList<DeviceInfo> &devices) {
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart jsonPart;
jsonPart.setHeader(QNetworkRequest::ContentTypeHeader, "application/json; charset=UTF-8");
jsonPart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="metadata")");
QJsonArray list;
for (const DeviceInfo &device: devices) {
QJsonObject obj;
obj["uniqueName"] = device.id;
obj["status"] = device.status;
obj["name"] = device.name;
obj["screenWidth"] = device.screenWidth;
obj["screenHeight"] = device.screenHeight;
obj["battery"] = device.battery;
list.append(obj);
}
jsonPart.setBody(QJsonDocument(list).toJson());
multiPart->append(jsonPart);
for (const DeviceInfo &device: devices) {
QHttpPart imagePart;
QString imageHeader = QStringLiteral("form-data; name=files; filename=%1.jpg").arg(device.id);
imagePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
imagePart.setHeader(QNetworkRequest::ContentDispositionHeader, imageHeader.toUtf8());
imagePart.setBody(device.image);
multiPart->append(imagePart);
}
QHttpPart uniquePart;
uniquePart.setHeader(QNetworkRequest::ContentDispositionHeader, R"(form-data; name="token")");
uniquePart.setBody(m_token.toUtf8());
multiPart->append(uniquePart);
QNetworkAccessManager manager; // создаём внутри потока
QUrl url(m_urlAppUpdate);
QNetworkRequest request(url);
// делаем запрос
QNetworkReply *reply = manager.post(request, multiPart);
multiPart->setParent(reply); // multiPart удалится, когда придёт ответ
// локальный цикл — ждём, пока придёт finished()
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) {
loop.quit();
});
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
// обрабатываем результат
if (!timeout.isActive()) {
qWarning() << "Таймаут postDevicesData:" << m_urlAppUpdate;
reply->abort();
} else if (reply->error() == QNetworkReply::NoError) {
QByteArray raw = reply->readAll();
QJsonParseError err;
QJsonDocument doc = QJsonDocument::fromJson(raw, &err);
if (err.error == QJsonParseError::NoError && doc.isObject()) {
QJsonObject root = doc.object();
if (!root.value("success").toBool()) {
qWarning() << root;
}
}
} else {
qWarning() << "Response error:" << reply->errorString() << m_urlAppUpdate;
}
reply->deleteLater();
}
QJsonValue DeviceScreener::apiPost(const QString &path, const QJsonObject &body) {
if (!m_manager) m_manager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl(m_apiBase + path));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization",
("Bearer " + SettingsDAO::get("access_token")).toUtf8());
QNetworkReply *reply = m_manager->post(request, QJsonDocument(body).toJson());
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result;
if (!timeout.isActive()) {
qCritical().noquote() << "DeviceScreener apiPost timeout:"
<< NetworkErrorUtils::describeClientTimeout(reply, path);
reply->abort();
} else if (reply->error() == QNetworkReply::NoError) {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isObject()) {
const QJsonObject root = doc.object();
if (root["success"].toBool()) result = root["data"];
else qCritical() << "DeviceScreener apiPost error:" << root;
}
} else {
qCritical().noquote() << "DeviceScreener apiPost error:"
<< NetworkErrorUtils::describeNetError(reply, path);
}
reply->deleteLater();
return result;
}
QString DeviceScreener::createDeviceOnApi(const DeviceInfo &device) {
const QString desktopId = SettingsDAO::get("desktop_id");
if (desktopId.isEmpty()) {
qWarning() << "createDeviceOnApi: desktop_id не задан";
return {};
}
QJsonObject body;
body["desktop_id"] = desktopId;
body["bank_profile_id"] = QJsonValue::Null;
body["status"] = "ONLINE";
body["name"] = device.name;
body["screen_width"] = device.screenWidth;
body["screen_height"] = device.screenHeight;
body["battery"] = device.battery;
const QJsonValue result = apiPost("/api/v1/device/", body);
if (!result.isNull() && !result.isUndefined()) {
return result.toObject()["id"].toString();
}
return {};
}
void DeviceScreener::dispatchScreenshotToMonitoring(const QString &desktopId,
const QString &deviceId,
const QString &name,
const QByteArray &jpeg) {
if (jpeg.isEmpty()) return;
// Отдельный поток + свой NetworkService, чтобы таймаут отправки не тормозил
// цикл сканера (паттерн LogArchiveSender::dispatch).
auto *thread = new QThread;
auto *svc = new NetworkService;
svc->moveToThread(thread);
QObject::connect(thread, &QThread::started, svc,
[svc, desktopId, deviceId, name, jpeg]() {
svc->postMonitoringScreenshot(desktopId, deviceId, name, jpeg);
emit svc->finished();
});
QObject::connect(svc, &NetworkService::finished, thread, &QThread::quit);
QObject::connect(svc, &NetworkService::finished, svc, &QObject::deleteLater);
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
void DeviceScreener::dispatchDeviceSyncToApi(const QString &apiBase,
const QString &accessToken,
const QString &apiId,
const QJsonObject &patchBody,
const QByteArray &image) {
if (apiId.isEmpty()) return;
// Отдельный поток + локальный QNetworkAccessManager, чтобы 30-секундный таймаут
// PATCH/POST не блокировал цикл опроса (паттерн dispatchScreenshotToMonitoring).
auto *thread = new QThread;
QObject::connect(thread, &QThread::started, [=]() {
QNetworkAccessManager manager; // живёт в этом потоке
const QByteArray auth = ("Bearer " + accessToken).toUtf8();
// Локальный цикл ожидания одного reply с таймаутом 30с.
auto waitReply = [](QNetworkReply *reply) {
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
QObject::connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
QObject::connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
if (!timeout.isActive()) reply->abort();
};
// PATCH статуса устройства.
{
QNetworkRequest request(QUrl(apiBase + "/api/v1/device/" + apiId));
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
request.setRawHeader("Authorization", auth);
QNetworkReply *reply = manager.sendCustomRequest(request, "PATCH", QJsonDocument(patchBody).toJson());
waitReply(reply);
if (reply->error() != QNetworkReply::NoError) {
qWarning().noquote() << "dispatchDeviceSyncToApi PATCH error:" << apiId << reply->errorString();
}
reply->deleteLater();
}
// POST скриншота (если есть).
if (!image.isEmpty()) {
auto *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType);
QHttpPart filePart;
filePart.setHeader(QNetworkRequest::ContentTypeHeader, "image/jpeg");
filePart.setHeader(QNetworkRequest::ContentDispositionHeader,
QStringLiteral("form-data; name=\"file\"; filename=\"screenshot.jpg\""));
filePart.setBody(image);
multiPart->append(filePart);
QNetworkRequest request(QUrl(apiBase + "/api/v1/device/add_screenshot/" + apiId));
request.setRawHeader("Authorization", auth);
QNetworkReply *reply = manager.post(request, multiPart);
multiPart->setParent(reply);
waitReply(reply);
if (reply->error() != QNetworkReply::NoError) {
qWarning().noquote() << "dispatchDeviceSyncToApi screenshot error:" << apiId << reply->errorString();
}
reply->deleteLater();
}
thread->quit();
});
QObject::connect(thread, &QThread::finished, thread, &QObject::deleteLater);
thread->start();
}
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device, const QByteArray &image) {
// Находим первый активный bank_profile для этого устройства (БД-запрос — должен
// выполняться на главном потоке, поэтому строим тело здесь).
QJsonValue bankProfileId = QJsonValue::Null;
const QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(device.id);
for (const BankProfileInfo &app : apps) {
if (!app.bankProfileId.isEmpty() && app.status == "active") {
bankProfileId = app.bankProfileId;
break;
}
}
QJsonObject body;
body["status"] = (device.status == "device") ? "ONLINE" : "OFFLINE";
body["bank_profile_id"] = bankProfileId;
body["battery"] = device.battery;
body["update_time"] = QDateTime::currentSecsSinceEpoch();
// Сетевую часть — fire-and-forget, чтобы таймаут не тормозил цикл опроса.
dispatchDeviceSyncToApi(m_apiBase, SettingsDAO::get("access_token"),
device.apiId, body, image);
}
QJsonValue DeviceScreener::apiGet(const QString &path) {
if (!m_manager) m_manager = new QNetworkAccessManager(this);
QNetworkRequest request(QUrl(m_apiBase + path));
request.setRawHeader("Authorization",
("Bearer " + SettingsDAO::get("access_token")).toUtf8());
QNetworkReply *reply = m_manager->get(request);
QEventLoop loop;
QTimer timeout;
timeout.setSingleShot(true);
connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
connect(reply, &QNetworkReply::errorOccurred, [&loop](QNetworkReply::NetworkError) { loop.quit(); });
connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
timeout.start(30000);
loop.exec();
QJsonValue result;
if (!timeout.isActive()) {
qCritical().noquote() << "DeviceScreener apiGet timeout:"
<< NetworkErrorUtils::describeClientTimeout(reply, path);
reply->abort();
} else if (reply->error() == QNetworkReply::NoError) {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isObject()) {
const QJsonObject root = doc.object();
if (root["success"].toBool()) result = root["data"];
else qCritical() << "DeviceScreener apiGet error:" << root;
}
} else {
qCritical().noquote() << "DeviceScreener apiGet error:"
<< NetworkErrorUtils::describeNetError(reply, path);
}
reply->deleteLater();
return result;
}
void DeviceScreener::start() {
// Пул для параллельного adb-опроса (фаза A). Лимит kMaxParallelScans, чтобы
// одновременные screencap'ы не насыщали USB-шину.
QThreadPool scanPool;
scanPool.setMaxThreadCount(kMaxParallelScans);
while (m_running) {
QList<QPair<QString, QString> > devices = readAdbDevices();
QSet<QString> liveSet; // устройства, видимые как "device" в текущем скане
const qint64 nowSec = QDateTime::currentSecsSinceEpoch();
// --- Фаза A: чистый adb-I/O параллельно по всем "device".
// Устройства не в статусе "device" (offline/unauthorized) игнорируем — они
// и раньше не попадали в liveSet и не меняли состояние, лишь уходили в
// офлайн по дебаунсу. Снимок кэша берём на главном потоке (read-only вход
// в потоки пула — без гонок).
QStringList deviceSerials;
for (const auto &[adbSerial, status]: devices) {
if (status == "device") deviceSerials << adbSerial;
}
const QMap<QString, DeviceInfo> cacheSnapshot = m_staticInfoCache;
// Какие устройства пора перечитать на список приложений (троттл).
QSet<QString> appsDue;
for (const QString &serial : deviceSerials) {
if (nowSec - m_lastAppsCheck.value(serial, 0) >= kAppsCheckIntervalSecs) {
appsDue.insert(serial);
}
}
// С каких устройств пора снять превью-кадр (троттл screencap, см.
// kScreencapIntervalSecs) — чтобы не грузить adb каждый цикл.
QSet<QString> shotDue;
for (const QString &serial : deviceSerials) {
if (nowSec - m_lastScreencap.value(serial, 0) >= kScreencapIntervalSecs) {
shotDue.insert(serial);
}
}
const QList<ScanResult> scans = QtConcurrent::blockingMapped<QList<ScanResult> >(
&scanPool, deviceSerials,
[&cacheSnapshot, &appsDue, &shotDue](const QString &serial) {
return scanDeviceIo(serial, cacheSnapshot.value(serial),
appsDue.contains(serial), shotDue.contains(serial));
});
// --- Фаза B: всё, что трогает БД/члены класса — последовательно в этом потоке.
for (const ScanResult &scan : scans) {
// android_id не прочитался — пропускаем устройство в этом цикле.
if (!scan.identified) continue;
DeviceInfo device = scan.info;
device.status = "device";
device.updateTime = QDateTime::currentDateTime();
// Обновляем кэш статики/battery (главный поток — безопасно).
m_staticInfoCache[scan.adbSerial] = scan.info;
// Проверяем: есть ли уже устройство с таким id в БД (с apiId)
const DeviceInfo existingById = DeviceDAO::getDeviceById(device.id);
if (!existingById.id.isEmpty() && !existingById.apiId.isEmpty()) {
device.apiId = existingById.apiId;
} else {
// Маппинг с восстановленным устройством — только по name (точное совпадение)
const DeviceInfo restored = DeviceDAO::findByName(device.name);
qDebug() << "[DeviceScreener] findByName(" << device.name << ") →"
<< "id:" << restored.id << "apiId:" << restored.apiId
<< "match:" << (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty());
if (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()) {
device.apiId = restored.apiId;
// Сначала создаём новое устройство (FK target), затем мигрируем, затем удаляем старое
DeviceDAO::upsertDevice(device);
const bool bpOk = BankProfileDAO::migrateDeviceId(restored.id, device.id);
const bool matOk = MaterialDAO::migrateDeviceId(restored.id, device.id);
qDebug() << "[DeviceScreener] migration result: bp=" << bpOk << "mat=" << matOk;
DeviceDAO::deleteDevice(restored.id);
qDebug() << "[DeviceScreener] matched restored device:" << restored.id << "->" << device.id
<< "apiId:" << device.apiId;
}
}
liveSet.insert(device.id);
// Отмечаем попытку захвата превью (даже если кадр пустой) — чтобы не
// дёргать screencap каждый цикл на устройстве, где он подвисает.
if (shotDue.contains(scan.adbSerial)) {
m_lastScreencap[scan.adbSerial] = nowSec;
}
device.image = scan.image;
if (!device.image.isEmpty()) {
if (!DeviceDAO::updateImage(device.id, device.image)) {
qDebug() << "Cant update screenshot";
}
}
if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update device";
}
// Проверяем установленные приложения по троттлу (список прочитан в фазе A).
// Пустой список = сбой adb — не двигаем метку, чтобы перечитать на след.
// цикле и не пометить все приложения как отсутствующие.
if (scan.appsRead && !scan.apps.isEmpty()) {
if (!BankProfileDAO::checkInstalledApps(device.id, scan.apps)) {
qWarning() << "Cant check installed apps";
}
m_lastAppsCheck[scan.adbSerial] = nowSec;
}
// Обновляем версии установленных банков (не чаще раза в 6 часов на профиль)
constexpr qint64 kAppVersionRefreshSecs = 6 * 60 * 60;
const QDateTime nowUtc = QDateTime::currentDateTimeUtc();
for (const BankProfileInfo &profile : BankProfileDAO::getApplicationsByDeviceId(device.id)) {
if (profile.package.isEmpty()) continue;
const bool stale = !profile.appVersionCheckedAt.isValid()
|| profile.appVersionCheckedAt.secsTo(nowUtc) >= kAppVersionRefreshSecs;
if (!stale) continue;
const QString ver = AdbUtils::getAppVersion(scan.adbSerial, profile.package);
if (ver.isEmpty()) continue; // не затираем известную версию пустотой
if (!BankProfileDAO::updateAppVersion(profile.id, ver)) {
qWarning() << "Cant update app_version for" << profile.code << profile.package;
}
}
// Синхронизация с API
DeviceInfo saved = DeviceDAO::getDeviceById(device.id);
if (saved.apiId.isEmpty() && !device.name.isEmpty()) {
// Устройство ещё не зарегистрировано — создаём
const QString apiId = createDeviceOnApi(device);
if (!apiId.isEmpty()) {
if (!DeviceDAO::updateApiId(device.id, apiId)) {
qWarning() << "Failed to save api_id for device:" << device.id;
}
qDebug() << "Device registered on API:" << device.id << "->" << apiId;
}
} else {
const qint64 now = QDateTime::currentSecsSinceEpoch();
// Основной API обновляем не чаще раза в 60 секунд.
if (now - m_lastApiUpdate.value(device.id, 0) >= 60) {
device.apiId = saved.apiId;
// PATCH статуса + POST скриншота уходят fire-and-forget внутри.
updateDeviceOnApi(device, device.image);
m_lastApiUpdate[device.id] = now;
}
// Отправка истории экрана в arc-monitoring отключена по запросу.
// Код сохранён закомментированным на случай возврата функционала.
// // История экрана на сервере логов: проверяем не чаще раза в
// // kMonitoringScreenIntervalSecs и шлём кадр только если экран
// // изменился относительно последнего отправленного (дедуп по sha256).
// if (!device.image.isEmpty()
// && now - m_lastMonitoringScreen.value(device.id, 0) >= kMonitoringScreenIntervalSecs) {
// m_lastMonitoringScreen[device.id] = now;
// const QByteArray hash =
// QCryptographicHash::hash(device.image, QCryptographicHash::Sha256);
// if (m_lastSentScreenHash.value(device.id) != hash) {
// m_lastSentScreenHash[device.id] = hash;
// dispatchScreenshotToMonitoring(SettingsDAO::get("desktop_id"),
// device.id, device.name, device.image);
// }
// }
}
}
// Detect transitions:
// 1) Online: устройство есть в liveSet, но ещё не в m_onlineDevices — мгновенно
for (const QString &id : liveSet) {
if (!m_onlineDevices.contains(id)) {
m_onlineDevices.insert(id);
m_offlineSince.remove(id);
qDebug() << "[DeviceScreener] device went ONLINE:" << id;
emit deviceWentOnline(id);
} else {
// Устройство всё ещё видно — сбрасываем pending offline-дебаунс (флап)
m_offlineSince.remove(id);
}
}
// 2) Offline: устройство было в m_onlineDevices, но больше не видно
// Засекаем m_offlineSince; срабатывает после 30s непрерывного отсутствия.
QSet<QString> goneOffline;
for (const QString &id : m_onlineDevices) {
if (liveSet.contains(id)) continue;
if (!m_offlineSince.contains(id)) {
m_offlineSince[id] = nowSec;
qDebug() << "[DeviceScreener] device missing, offline debounce started:" << id;
} else if (nowSec - m_offlineSince[id] >= kOfflineDebounceSecs) {
goneOffline.insert(id);
}
}
for (const QString &id : goneOffline) {
m_onlineDevices.remove(id);
m_offlineSince.remove(id);
// Сбрасываем кэш статики для этого устройства: при переподключении по тому
// же adbSerial может оказаться другой аппарат — перечитаем заново.
for (auto it = m_staticInfoCache.begin(); it != m_staticInfoCache.end();) {
if (it.value().id == id) {
it = m_staticInfoCache.erase(it);
} else {
++it;
}
}
qDebug() << "[DeviceScreener] device went OFFLINE (debounced):" << id;
// Сообщаем серверу что устройство офлайн (отдельно от bank_profile-синка)
const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id);
if (!offlineDevice.apiId.isEmpty()) {
DeviceInfo offlineCopy = offlineDevice;
offlineCopy.status = "offline";
updateDeviceOnApi(offlineCopy);
}
emit deviceWentOffline(id);
}
// БД status: считаем онлайном только debounced-онлайн.
QStringList debouncedOnlineList;
for (const QString &id : m_onlineDevices) debouncedOnlineList << id;
if (!DeviceDAO::markDevicesOfflineExcept(debouncedOnlineList)) {
qDebug() << "Cant mark devices offline";
}
if (m_firstScan) {
m_firstScan = false;
qDebug() << "[DeviceScreener] first pass done, online devices:" << m_onlineDevices;
emit firstPassDone();
}
QThread::msleep(1000);
}
emit finished();
}
QByteArray convertPngToJpeg(const QByteArray &pngData, const int quality = 90) {
QImage image;
if (!image.loadFromData(pngData, "PNG")) {
qWarning() << "Не удалось загрузить PNG из байт";
return {};
}
QByteArray jpgData;
QBuffer buffer(&jpgData);
buffer.open(QIODevice::WriteOnly);
image.save(&buffer, "JPG", quality);
return jpgData;
}
QByteArray DeviceScreener::takeScreenshot(const QString &deviceId) {
QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
QStringList arguments;
arguments << "-s" << deviceId << "exec-out" << "screencap" << "-p";
process.start(AdbUtils::adbPath(), arguments);
if (!process.waitForFinished(5000)) {
qWarning() << "ADB скриншот не завершился для" << deviceId;
process.kill();
process.waitForFinished(1000);
return {};
}
const QByteArray imageData = process.readAllStandardOutput();
if (imageData.isEmpty()) {
qWarning() << "Скриншот пустой для " << deviceId;
return {};
}
return convertPngToJpeg(imageData, 90);
}
QList<QPair<QString, QString> > DeviceScreener::readAdbDevices() {
QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
process.start(AdbUtils::adbPath(), QStringList() << "devices");
if (!process.waitForStarted(5000)) {
qWarning() << "[DeviceScreener] Failed to start adb devices:" << process.errorString();
return {};
}
if (!process.waitForFinished(10000)) {
qWarning() << "[DeviceScreener] adb devices timeout";
process.kill();
process.waitForFinished(1000);
return {};
}
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 — устройство в режиме восстановления.
*/
// Разделяем строку на id и type по табуляции
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;
}
static const QRegularExpression screenRegex(R"(Physical size: (\d+)x(\d+))");
static const QRegularExpression batteryRegex(R"(level:\s*(\d+))");
DeviceScreener::ScanResult DeviceScreener::scanDeviceIo(const QString &adbSerial,
const DeviceInfo &cachedStatic,
bool readApps, bool takeShot) {
ScanResult result;
result.adbSerial = adbSerial;
// ok=false означает, что процесс не завершился за таймаут (зависшая команда):
// результат считаем «нет данных», а не валидной пустой строкой, чтобы вызывающий
// код не затирал известные значения.
auto runAdbCommand = [&](const QStringList &args, bool *ok = nullptr) -> QString {
QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args);
if (!process.waitForFinished(kAdbCommandTimeoutMs)) {
qWarning() << "[DeviceScreener] adb command timeout for" << adbSerial << args;
process.kill();
process.waitForFinished(1000);
if (ok) *ok = false;
return {};
}
if (ok) *ok = true;
return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
};
// Статика (id/name/android/размер экрана) не меняется за сессию. Если передан
// непустой кэш — переиспользуем его и не дёргаем adb. Иначе читаем с нуля.
DeviceInfo info = cachedStatic;
if (info.id.isEmpty()) {
bool idOk = false;
bool modelOk = false;
const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"}, &idOk);
const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"}, &modelOk);
// Идентичность не прочиталась (таймаут/пусто) — НЕ подменяем id на adbSerial.
// Возвращаем identified=false; вызывающий код пропустит устройство в этом
// цикле и повторит на следующем.
if (!idOk || androidId.isEmpty()) {
qWarning() << "[DeviceScreener] device not identified yet (android_id unavailable):" << adbSerial;
info.adbSerial = adbSerial;
info.updateTime = QDateTime::currentDateTime();
result.info = info;
result.identified = false;
return result;
}
info.id = androidId;
info.adbSerial = adbSerial;
info.name = (modelOk && !model.isEmpty() ? model : QStringLiteral("device")) + "-" + info.id;
info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"});
const QString screenOutput = runAdbCommand({"shell", "wm", "size"});
if (QRegularExpressionMatch match = screenRegex.match(screenOutput); match.hasMatch()) {
info.screenWidth = match.captured(1).toInt();
info.screenHeight = match.captured(2).toInt();
}
}
result.identified = true;
// Battery — динамическое, читаем каждый цикл. На таймаут/непрочитанное значение
// оставляем прошлое (info уже содержит закэшированное), а не пишем 0.
bool batteryOk = false;
const QString batteryOutput = runAdbCommand({"shell", "dumpsys", "battery"}, &batteryOk);
if (batteryOk) {
if (QRegularExpressionMatch match = batteryRegex.match(batteryOutput); match.hasMatch()) {
info.battery = match.captured(1).toInt();
}
}
info.updateTime = QDateTime::currentDateTime();
result.info = info;
// Тяжёлый чистый adb-I/O — превью-кадр и список приложений по троттлу.
// takeShot=false → кадр не снимаем (result.image пустой), фаза B просто не
// обновит превью в этом цикле; следующий захват — через kScreencapIntervalSecs.
if (takeShot) {
result.image = takeScreenshot(adbSerial);
}
if (readApps) {
result.apps = AdbUtils::getListApps(adbSerial);
result.appsRead = true;
}
return result;
}
void DeviceScreener::stop() {
m_running = false;
}