1
0
forked from BRT/arc

Add app_version tracking: database schema migration, ADB utility support, and UI enhancements.

This commit is contained in:
slava 2026-05-18 18:42:59 +05:00
parent 0adb57d87e
commit 7bf49ba749
14 changed files with 170 additions and 32 deletions

View File

@ -740,6 +740,21 @@ QString AdbUtils::getDeviceTimezone(const QString &deviceId) {
return out.trimmed();
}
QString AdbUtils::getAppVersion(const QString &deviceId, const QString &packageName) {
if (deviceId.isEmpty() || packageName.isEmpty()) {
return {};
}
QString out;
if (!startProcess(adbPath(),
{"-s", deviceId, "shell", "dumpsys", "package", packageName},
&out)) {
return {};
}
static const QRegularExpression re(R"(versionName=([^\s]+))");
const QRegularExpressionMatch m = re.match(out);
return m.hasMatch() ? m.captured(1).trimmed() : QString();
}
void AdbUtils::unlockScreen(const QString &deviceId, const int screenWidth, const int screenHeight) {
const QString adb = adbPath();

View File

@ -56,6 +56,9 @@ public:
// Возвращает таймзону устройства (напр. "Asia/Bangkok"), либо пустую строку при ошибке
static QString getDeviceTimezone(const QString &deviceId);
// Возвращает versionName установленного пакета (напр. "19.17.0"), либо пустую строку при ошибке/отсутствии
static QString getAppVersion(const QString &deviceId, const QString &packageName);
private:
static QString adbDir();
static bool startProcess(const QString &program, const QStringList &args,

View File

@ -598,7 +598,7 @@ void GetLastTransactionsScript::doStart() {
}
}
// Проверка времени по HH:MM:SS (±1 час, с учётом wraparound полуночи)
// Проверка времени по HH:MM:SS (±10 минут, с учётом wraparound полуночи)
if (m_searchTime.isValid() && !tx.time.isEmpty()) {
const QTime listTime = QTime::fromString(tx.time, "HH:mm:ss");
const QTime searchLocalTime = m_searchTime.toTimeZone(tjTz).time();
@ -642,26 +642,28 @@ void GetLastTransactionsScript::doStart() {
xmlScreenParser.parseAndSaveXml(AdbUtils::getScreenDumpAsXml(m_deviceId, ++m_counter));
closeGooglePlayBannerIfVisible(m_deviceId, device.screenWidth, device.screenHeight);
for (const Node &n : xmlScreenParser.findAllNodes()) {
if (!n.contentDesc.contains(QString::fromUtf8("Успешный платеж"))) continue;
if (!n.contentDesc.contains(QString::fromUtf8("Дата:"))) continue;
// Принимаем оба формата детальной страницы:
// — "Успешный платеж" (исходящий) с ключами "Дата:" / "Время:"
// — "Поступление" (входящий) с ключами "Дата" / "Время"
// Гейт — наличие даты И времени в любой форме.
if (!n.contentDesc.contains(QString::fromUtf8("Дата"))) continue;
if (!n.contentDesc.contains(QString::fromUtf8("Время"))) continue;
const QStringList lines = n.contentDesc.split('\n');
for (int i = 0; i < lines.size(); ++i) {
const QString key = lines[i].trimmed();
QString key = lines[i].trimmed();
if (key.endsWith(':')) key.chop(1); // нормализуем "Дата:" → "Дата"
if (i + 1 >= lines.size()) continue;
const QString val = lines[i + 1].trimmed();
if (key == QString::fromUtf8("Получатель:")) {
if (key == QString::fromUtf8("Получатель")) {
detail.receiverAccount = val;
} else if (key == QString::fromUtf8("Сумма:")) {
} else if (key == QString::fromUtf8("Сумма")) {
detail.amount = val.toDouble();
} else if (key == QString::fromUtf8("Дата:")) {
} else if (key == QString::fromUtf8("Дата")) {
detail.date = val;
} else if (key == QString::fromUtf8("Время:")) {
} else if (key == QString::fromUtf8("Время")) {
detail.time = val;
}
}
if (n.contentDesc.contains(QString::fromUtf8("Успешный"))) {
detail.status = QString::fromUtf8("Успешный");
}
detailLoaded = true;
break;
}
@ -683,7 +685,7 @@ void GetLastTransactionsScript::doStart() {
<< "date=" << detail.date << "time=" << detail.time
<< "amount=" << detail.amount << "receiver=" << detail.receiverAccount;
// Проверяем время ±1 час
// Проверяем время ±10 минут
if (m_searchTime.isValid() && !detail.date.isEmpty() && !detail.time.isEmpty()) {
QDateTime txTime = QDateTime::fromString(
detail.date + " " + detail.time, "dd.MM.yyyy HH:mm:ss");

View File

@ -27,6 +27,7 @@ currency=ARS
country=argentina
name=Black
phone_code=54
app_version=0
[ozon]
android_package_name=ru.ozon.fintech.finance
@ -34,6 +35,7 @@ currency=RUB
country=russia
name=Ozon
phone_code=7
app_version=19.17.0
[dushanbe_city_bank]
android_package_name=tj.dc.next1
@ -41,3 +43,4 @@ currency=TJS
country=tajikistan
name=Dushanbe City
phone_code=992
app_version=0

View File

@ -119,10 +119,16 @@ void DatabaseManager::initSchema() {
bank_profile_id TEXT DEFAULT '',
currency TEXT DEFAULT '',
amount REAL DEFAULT 0,
app_version TEXT DEFAULT '',
app_version_checked_at DATETIME,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, code)
))");
// Миграция: добавляем колонки app_version и app_version_checked_at если их нет (ошибка = уже существует)
q.exec("ALTER TABLE bank_profiles ADD COLUMN app_version TEXT DEFAULT ''");
q.exec("ALTER TABLE bank_profiles ADD COLUMN app_version_checked_at DATETIME");
q.exec(R"(CREATE TABLE IF NOT EXISTS materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
device_id TEXT,

View File

@ -151,6 +151,8 @@ BankProfileInfo parseApplication(const QSqlQuery &query) {
app.bankProfileId = query.value("bank_profile_id").toString();
app.currency = query.value("currency").toString();
app.amount = query.value("amount").toDouble();
app.appVersion = query.value("app_version").toString();
app.appVersionCheckedAt = query.value("app_version_checked_at").toDateTime();
return app;
}
@ -158,7 +160,7 @@ BankProfileInfo BankProfileDAO::getApplication(const QString &deviceId, const QS
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at
FROM bank_profiles
WHERE device_id = :device_id AND code = :code
LIMIT 1
@ -183,7 +185,7 @@ QList<BankProfileInfo> BankProfileDAO::getApplicationsByDeviceId(const QString &
QList<BankProfileInfo> list;
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at
FROM bank_profiles
WHERE device_id = :device_id AND install = 'yes'
)");
@ -206,7 +208,7 @@ QList<BankProfileInfo> BankProfileDAO::getAllApplicationsByDeviceId(const QStrin
QList<BankProfileInfo> list;
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at
FROM bank_profiles
WHERE device_id = :device_id
)");
@ -262,6 +264,23 @@ bool BankProfileDAO::updateAmount(const int appId, const double amount) {
return true;
}
bool BankProfileDAO::updateAppVersion(const int appId, const QString &appVersion) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
UPDATE bank_profiles
SET app_version = :app_version, app_version_checked_at = :checked_at
WHERE id = :id
)");
query.bindValue(":app_version", appVersion);
query.bindValue(":checked_at", QDateTime::currentDateTimeUtc());
query.bindValue(":id", appId);
if (!query.exec()) {
qWarning() << "Ошибка при обновлении app_version:" << query.lastError().text();
return false;
}
return true;
}
bool BankProfileDAO::updateBankProfileId(const int appId, const QString &bankProfileId) {
QSqlQuery query(DatabaseManager::instance().database());
query.prepare("UPDATE bank_profiles SET bank_profile_id = :bank_profile_id WHERE id = :id");
@ -278,7 +297,7 @@ QList<BankProfileInfo> BankProfileDAO::findAllByBankProfileId(const QString &ban
QList<BankProfileInfo> list;
QSqlQuery query(DatabaseManager::instance().database());
query.prepare(R"(
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount
SELECT id, device_id, pin_code, code, name, package, status, comment, install, pin_code_checked, phone, email, full_name, bank_profile_id, currency, amount, app_version, app_version_checked_at
FROM bank_profiles
WHERE bank_profile_id = :bank_profile_id
)");

View File

@ -36,6 +36,8 @@ public:
[[nodiscard]] static bool updateAmount(int appId, double amount);
[[nodiscard]] static bool updateAppVersion(int appId, const QString &appVersion);
[[nodiscard]] static BankProfileInfo getApplication(const QString &deviceId, const QString &appCode);
[[nodiscard]] static QList<BankProfileInfo> getApplicationsByDeviceId(const QString &deviceId);

BIN
images/copy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -36,6 +36,8 @@ CREATE TABLE IF NOT EXISTS bank_profiles
bank_profile_id TEXT DEFAULT '',
currency TEXT DEFAULT '',
amount REAL DEFAULT 0,
app_version TEXT DEFAULT '',
app_version_checked_at DATETIME,
FOREIGN KEY (device_id) REFERENCES devices (id) ON DELETE CASCADE,
UNIQUE (device_id, code)
);

View File

@ -21,5 +21,7 @@ struct BankProfileInfo {
QString bankProfileId;
QString currency;
double amount = 0.0;
QString appVersion;
QDateTime appVersionCheckedAt;
QDateTime updateTime;
};

View File

@ -378,6 +378,22 @@ void DeviceScreener::start() {
if (!BankProfileDAO::checkInstalledApps(device.id, apps)) {
qWarning() << "Cant check installed apps";
}
// Обновляем версии установленных банков (не чаще раза в 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(adbSerial, profile.package);
if (ver.isEmpty()) continue; // не затираем известную версию пустотой
if (!BankProfileDAO::updateAppVersion(profile.id, ver)) {
qWarning() << "Cant update app_version for" << profile.code << profile.package;
}
}
}
// Синхронизация с API

View File

@ -9,6 +9,10 @@
#include <QInputDialog>
#include <QThread>
#include <QTimer>
#include <QGuiApplication>
#include <QClipboard>
#include <QToolTip>
#include <QIcon>
#include "ozon/LoginAndCheckAccountsScript.h"
#include "ozon/GetProfileInfoScript.h"
@ -81,10 +85,28 @@ AccountSettingsWindow::AccountSettingsWindow(
layout->addLayout(pinRow);
// ── Профиль ────────────────────────────────────────────────────────
auto *profileRow = new QHBoxLayout;
m_profileLabel = new QLabel;
m_profileLabel->setTextFormat(Qt::RichText);
m_profileLabel->setStyleSheet("margin: 4px 0;");
layout->addWidget(m_profileLabel);
profileRow->addWidget(m_profileLabel);
m_copyPhoneBtn = new QPushButton;
m_copyPhoneBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png"));
m_copyPhoneBtn->setIconSize(QSize(16, 16));
m_copyPhoneBtn->setToolTip("Копировать телефон");
m_copyPhoneBtn->setFixedSize(28, 24);
m_copyPhoneBtn->setStyleSheet("padding: 2px;");
m_copyPhoneBtn->hide();
connect(m_copyPhoneBtn, &QPushButton::clicked, this, [this]() {
if (m_app.phone.isEmpty()) return;
QGuiApplication::clipboard()->setText(m_app.phone);
QToolTip::showText(m_copyPhoneBtn->mapToGlobal(QPoint(0, m_copyPhoneBtn->height())),
"Скопировано", m_copyPhoneBtn, {}, 1500);
});
profileRow->addWidget(m_copyPhoneBtn);
profileRow->addStretch();
layout->addLayout(profileRow);
// ── Кнопки обновления ──────────────────────────────────────────────
auto *actionsRow = new QHBoxLayout;
@ -197,6 +219,7 @@ void AccountSettingsWindow::updateStatusUI() {
: " <span style='color:green;'>&#9679; Синхронизирован</span>";
m_profileLabel->setText(text + syncText);
}
m_copyPhoneBtn->setVisible(!m_app.phone.isEmpty());
}
// ── Toggle On/Off ──────────────────────────────────────────────────────────
@ -571,9 +594,9 @@ void AccountSettingsWindow::reloadCards() {
m_cardsTable->clearContents();
m_cardsTable->setRowCount(accounts.size());
m_cardsTable->setColumnCount(7);
m_cardsTable->setColumnCount(8);
m_cardsTable->setHorizontalHeaderLabels({
"Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", ""
"Сервер", "Статус", "Время обновления", "Номер карты", "Сумма", "Валюта", "", ""
});
m_cardsTable->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
@ -598,11 +621,32 @@ void AccountSettingsWindow::reloadCards() {
timeItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_cardsTable->setItem(k, 2, timeItem);
// Номер карты
// Номер карты (текст + иконка копирования)
const QString displayNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber;
auto *numItem = new QTableWidgetItem(displayNumber);
numItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
m_cardsTable->setItem(k, 3, numItem);
const QString fullCardNumber = acc.cardNumber.isEmpty() ? acc.lastNumbers : acc.cardNumber;
auto *cardCell = new QWidget(m_cardsTable);
auto *cardLayout = new QHBoxLayout(cardCell);
cardLayout->setContentsMargins(4, 0, 4, 0);
cardLayout->setSpacing(6);
auto *cardLabel = new QLabel(displayNumber, cardCell);
cardLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
cardLayout->addWidget(cardLabel);
if (!fullCardNumber.isEmpty()) {
auto *copyCardBtn = new QPushButton(cardCell);
copyCardBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png"));
copyCardBtn->setIconSize(QSize(14, 14));
copyCardBtn->setToolTip("Копировать номер карты");
copyCardBtn->setFixedSize(22, 20);
copyCardBtn->setStyleSheet("padding: 1px; border: none;");
connect(copyCardBtn, &QPushButton::clicked, this, [copyCardBtn, fullCardNumber]() {
QGuiApplication::clipboard()->setText(fullCardNumber);
QToolTip::showText(copyCardBtn->mapToGlobal(QPoint(0, copyCardBtn->height())),
"Скопировано", copyCardBtn, {}, 1500);
});
cardLayout->addWidget(copyCardBtn);
}
cardLayout->addStretch();
m_cardsTable->setCellWidget(k, 3, cardCell);
// Сумма
auto *amountItem = new QTableWidgetItem(QString::number(acc.amount));
@ -624,6 +668,23 @@ void AccountSettingsWindow::reloadCards() {
toggleMaterialStatus(acc);
});
}
// Кнопка копирования номера счёта
if (!acc.accountNumber.isEmpty()) {
auto *copyAcctBtn = new QPushButton(m_cardsTable);
copyAcctBtn->setIcon(QIcon(QCoreApplication::applicationDirPath() + "/images/copy.png"));
copyAcctBtn->setIconSize(QSize(16, 16));
copyAcctBtn->setToolTip("Копировать номер счёта");
copyAcctBtn->setFixedSize(28, 24);
copyAcctBtn->setStyleSheet("padding: 2px;");
m_cardsTable->setCellWidget(k, 7, copyAcctBtn);
connect(copyAcctBtn, &QPushButton::clicked, this, [this, copyAcctBtn, acc]() {
QGuiApplication::clipboard()->setText(acc.accountNumber);
QToolTip::showText(copyAcctBtn->mapToGlobal(QPoint(0, copyAcctBtn->height())),
"Скопировано", copyAcctBtn, {}, 1500);
});
}
}
m_cardsTable->resizeColumnsToContents();

View File

@ -24,6 +24,7 @@ private:
QLabel *m_statusLabel;
QLabel *m_pinLabel;
QLabel *m_profileLabel;
QPushButton *m_copyPhoneBtn = nullptr;
QPushButton *m_toggleBtn;
QPushButton *m_profileBtn;
QPushButton *m_cardsBtn;

View File

@ -110,13 +110,13 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
tableWidget->clearContents();
tableWidget->setRowCount(apps.size());
tableWidget->setColumnCount(7);
tableWidget->setHorizontalHeaderLabels({"", "Приложение", "Пакет", "Валюта", "Статус", "Комментарий", ""});
tableWidget->setColumnCount(8);
tableWidget->setHorizontalHeaderLabels({"", "Приложение", "Пакет", "Валюта", "Статус", "Версия на телефоне", "Версия ARC", ""});
tableWidget->horizontalHeader()->setDefaultAlignment(Qt::AlignLeft | Qt::AlignVCenter);
for (int i = 0; i < 7; ++i)
for (int i = 0; i < 8; ++i)
tableWidget->horizontalHeader()->setSectionResizeMode(i, QHeaderView::ResizeToContents);
tableWidget->horizontalHeader()->setSectionResizeMode(5, QHeaderView::Stretch);
tableWidget->horizontalHeader()->setSectionResizeMode(2, QHeaderView::Stretch);
for (int k = 0; k < apps.size(); ++k) {
const BankProfileInfo &app = apps[k];
@ -153,16 +153,22 @@ void DeviceSettingsWindow::reloadContent(QTableWidget *tableWidget) {
statusItem->setForeground(QBrush(Qt::darkGreen));
tableWidget->setItem(k, 4, statusItem);
// Комментарий
auto *commentItem = new QTableWidgetItem(app.comment);
commentItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
tableWidget->setItem(k, 5, commentItem);
// Версия на телефоне (из БД, обновляется через adb)
auto *deviceVersionItem = new QTableWidgetItem(app.appVersion);
deviceVersionItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
tableWidget->setItem(k, 5, deviceVersionItem);
// Версия ARC (из config.ini)
const QString arcVersion = configSettings.value(app.code + "/app_version").toString();
auto *arcVersionItem = new QTableWidgetItem(arcVersion);
arcVersionItem->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter);
tableWidget->setItem(k, 6, arcVersionItem);
// Кнопка "Настроить"
if (app.install) {
auto *settingsBtn = new QPushButton("Настроить", tableWidget);
settingsBtn->setStyleSheet("background-color: green; color: white; border-radius: 4px; height: 26px;");
tableWidget->setCellWidget(k, 6, settingsBtn);
tableWidget->setCellWidget(k, 7, settingsBtn);
connect(settingsBtn, &QPushButton::clicked, this, [this, app, tableWidget]() {
const BankProfileInfo dbApp = BankProfileDAO::getApplication(m_deviceId, app.code);