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

551 lines
22 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 <QTimer>
#include <QImage>
#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"
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() << "DeviceScreener apiPost timeout:" << 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() << "DeviceScreener apiPost network error:" << reply->errorString();
}
reply->deleteLater();
return result;
}
void DeviceScreener::apiPatch(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->sendCustomRequest(request, "PATCH", 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();
if (!timeout.isActive()) {
qCritical() << "DeviceScreener apiPatch timeout:" << path;
reply->abort();
} else if (reply->error() != QNetworkReply::NoError) {
qCritical() << "DeviceScreener apiPatch network error:" << reply->errorString();
}
reply->deleteLater();
}
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::postScreenshotToApi(const QString &apiId, const QByteArray &image) {
if (!m_manager) m_manager = new QNetworkAccessManager(this);
const QString path = "/api/v1/device/add_screenshot/" + apiId;
QNetworkRequest request(QUrl(m_apiBase + path));
request.setRawHeader("Authorization",
("Bearer " + SettingsDAO::get("access_token")).toUtf8());
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);
QNetworkReply *reply = m_manager->post(request, multiPart);
multiPart->setParent(reply);
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();
if (!timeout.isActive()) {
qWarning() << "postScreenshotToApi timeout for device" << apiId;
reply->abort();
} else if (reply->error() != QNetworkReply::NoError) {
qWarning() << "postScreenshotToApi network error:" << reply->errorString();
} else {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isObject() && doc.object()["success"].toBool()) {
// ok
} else {
qWarning() << "postScreenshotToApi failed for device" << apiId << ":" << doc.toJson(QJsonDocument::Compact);
}
}
reply->deleteLater();
}
void DeviceScreener::updateDeviceOnApi(const DeviceInfo &device) {
// Находим первый активный 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();
apiPatch("/api/v1/device/" + device.apiId, body);
}
void DeviceScreener::toggleBankProfilesOnApi(const QString &deviceId, bool enable) {
if (enable) return; // не включаем автоматически при появлении устройства
const QList<BankProfileInfo> apps = BankProfileDAO::getAllApplicationsByDeviceId(deviceId);
for (const BankProfileInfo &app : apps) {
if (app.bankProfileId.isEmpty()) continue;
if (app.status != "active") continue;
apiPatch("/api/v1/profile/bank_profile/" + app.bankProfileId + "/false", QJsonObject());
const QList<MaterialInfo> materials = MaterialDAO::getAccounts(deviceId, app.code);
for (const MaterialInfo &mat : materials) {
if (mat.materialId.isEmpty()) continue;
apiPatch("/api/v1/profile/material/" + mat.materialId + "/false", QJsonObject());
}
}
BankProfileDAO::deactivateAppsForDevice(deviceId);
}
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() << "DeviceScreener apiGet timeout:" << 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() << "DeviceScreener apiGet network error:" << reply->errorString();
}
reply->deleteLater();
return result;
}
void DeviceScreener::start() {
while (m_running) {
// Устройства, которые были онлайн в БД до текущего скана
const QStringList prevOnline = DeviceDAO::getOnlineDeviceCodes();
QList<QPair<QString, QString> > devices = readAdbDevices();
QStringList onlineIds;
QList<DeviceInfo> deviceInfos;
for (const auto &[adbSerial, status]: devices) {
DeviceInfo device;
bool isChecked = true;
if (status == "device") {
device = readDeviceInfo(adbSerial);
// Проверяем: есть ли уже устройство с таким 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);
if (!restored.id.isEmpty() && restored.id != device.id && !restored.apiId.isEmpty()) {
device.apiId = restored.apiId;
DeviceDAO::deleteDevice(restored.id);
qDebug() << "[DeviceScreener] matched restored device:" << restored.id << "->" << device.id
<< "apiId:" << device.apiId;
}
}
onlineIds << device.id;
isChecked = prevOnline.contains(device.id);
device.image = takeScreenshot(adbSerial);
if (!device.image.isEmpty()) {
if (!DeviceDAO::updateImage(device.id, device.image)) {
qDebug() << "Cant update screenshot";
}
}
} else {
device.id = adbSerial;
}
device.status = status;
device.updateTime = QDateTime::currentDateTime();
deviceInfos.append(device);
if (status == "device") {
if (!DeviceDAO::upsertDevice(device)) {
qDebug() << "Cant update device";
}
if (!isChecked) {
// Новое устройство — проверяем приложения (после upsert, чтобы FK не падал)
QSet<QString> apps = AdbUtils::getListApps(adbSerial);
if (!BankProfileDAO::checkInstalledApps(device.id, apps)) {
qWarning() << "Cant check installed apps";
} else {
qDebug() << "Apps checked:" << device.id;
}
}
}
// Синхронизация с API
DeviceInfo saved = DeviceDAO::getDeviceById(device.id);
if (saved.apiId.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 if (status == "device") {
// Обновляем не чаще раза в 60 секунд
const qint64 now = QDateTime::currentSecsSinceEpoch();
if (now - m_lastApiUpdate.value(device.id, 0) >= 60) {
device.apiId = saved.apiId;
updateDeviceOnApi(device);
if (!device.image.isEmpty()) {
postScreenshotToApi(saved.apiId, device.image);
}
m_lastApiUpdate[device.id] = now;
}
}
}
if (!DeviceDAO::markDevicesOfflineExcept(onlineIds)) {
qDebug() << "Cant mark devices offline";
}
// Было онлайн в БД, сейчас офлайн — выключаем профили и обновляем статус на сервере
for (const QString &id : prevOnline) {
if (!onlineIds.contains(id)) {
toggleBankProfilesOnApi(id, false);
// Отправляем OFFLINE статус на сервер
const DeviceInfo offlineDevice = DeviceDAO::getDeviceById(id);
if (!offlineDevice.apiId.isEmpty()) {
DeviceInfo offlineCopy = offlineDevice;
offlineCopy.status = "offline";
updateDeviceOnApi(offlineCopy);
}
}
}
if (m_firstScan) {
m_firstScan = false;
}
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;
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 {};
}
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 — устройство в режиме восстановления.
*/
// Разделяем строку на 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+))");
DeviceInfo DeviceScreener::readDeviceInfo(const QString &adbSerial) {
DeviceInfo info;
info.screenWidth = 0;
info.screenHeight = 0;
info.battery = 0;
auto runAdbCommand = [&](const QStringList &args) -> QString {
QProcess process;
process.setWorkingDirectory(QFileInfo(AdbUtils::adbPath()).absolutePath());
process.start(AdbUtils::adbPath(), QStringList{"-s", adbSerial} + args);
process.waitForFinished();
return QString::fromUtf8(process.readAllStandardOutput()).trimmed();
};
const QString androidId = runAdbCommand({"shell", "settings", "get", "secure", "android_id"});
const QString model = runAdbCommand({"shell", "getprop", "ro.product.model"});
info.id = androidId.isEmpty() ? adbSerial : androidId;
info.adbSerial = adbSerial;
info.name = model + "-" + info.id;
info.android = runAdbCommand({"shell", "getprop", "ro.build.version.release"});
// Размер экрана
const QString screenOutput = runAdbCommand({"shell", "wm", "size"});
QRegularExpressionMatch match = screenRegex.match(screenOutput);
if (match.hasMatch()) {
info.screenWidth = match.captured(1).toInt();
info.screenHeight = match.captured(2).toInt();
}
// Уровень батареи
const QString batteryOutput = runAdbCommand({"shell", "dumpsys", "battery"});
match = batteryRegex.match(batteryOutput);
if (match.hasMatch()) {
info.battery = match.captured(1).toInt();
}
info.updateTime = QDateTime::currentDateTime();
return info;
}
void DeviceScreener::stop() {
m_running = false;
}