diff --git a/android/adb/AdbUtils.cpp b/android/adb/AdbUtils.cpp index 2887a8e..f8063c3 100644 --- a/android/adb/AdbUtils.cpp +++ b/android/adb/AdbUtils.cpp @@ -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(); diff --git a/android/adb/AdbUtils.h b/android/adb/AdbUtils.h index c8a5ad1..154ff6f 100644 --- a/android/adb/AdbUtils.h +++ b/android/adb/AdbUtils.h @@ -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, diff --git a/android/dushanbe/GetLastTransactionsScript.cpp b/android/dushanbe/GetLastTransactionsScript.cpp index 92bc82f..560ac32 100644 --- a/android/dushanbe/GetLastTransactionsScript.cpp +++ b/android/dushanbe/GetLastTransactionsScript.cpp @@ -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"); diff --git a/config.ini b/config.ini index ef93d27..4123663 100644 --- a/config.ini +++ b/config.ini @@ -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 diff --git a/database/DatabaseManager.cpp b/database/DatabaseManager.cpp index 86ab677..18c975d 100644 --- a/database/DatabaseManager.cpp +++ b/database/DatabaseManager.cpp @@ -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, diff --git a/database/dao/BankProfileDAO.cpp b/database/dao/BankProfileDAO.cpp index 2063b80..075d775 100644 --- a/database/dao/BankProfileDAO.cpp +++ b/database/dao/BankProfileDAO.cpp @@ -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 BankProfileDAO::getApplicationsByDeviceId(const QString & QList 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 BankProfileDAO::getAllApplicationsByDeviceId(const QStrin QList 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 BankProfileDAO::findAllByBankProfileId(const QString &ban QList 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 )"); diff --git a/database/dao/BankProfileDAO.h b/database/dao/BankProfileDAO.h index 7d98d5a..e0b8d53 100644 --- a/database/dao/BankProfileDAO.h +++ b/database/dao/BankProfileDAO.h @@ -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 getApplicationsByDeviceId(const QString &deviceId); diff --git a/images/copy.png b/images/copy.png new file mode 100644 index 0000000..50afd5e Binary files /dev/null and b/images/copy.png differ diff --git a/migrations.sql b/migrations.sql index 60aa02f..a6019ab 100644 --- a/migrations.sql +++ b/migrations.sql @@ -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) ); diff --git a/models/db/BankProfileInfo.h b/models/db/BankProfileInfo.h index 0304f5b..148fac8 100644 --- a/models/db/BankProfileInfo.h +++ b/models/db/BankProfileInfo.h @@ -21,5 +21,7 @@ struct BankProfileInfo { QString bankProfileId; QString currency; double amount = 0.0; + QString appVersion; + QDateTime appVersionCheckedAt; QDateTime updateTime; }; diff --git a/services/DeviceScreener.cpp b/services/DeviceScreener.cpp index ba8a707..bbbbb2b 100644 --- a/services/DeviceScreener.cpp +++ b/services/DeviceScreener.cpp @@ -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 diff --git a/views/bank/AccountSettingsWindow.cpp b/views/bank/AccountSettingsWindow.cpp index 71c48ff..52b61fc 100644 --- a/views/bank/AccountSettingsWindow.cpp +++ b/views/bank/AccountSettingsWindow.cpp @@ -9,6 +9,10 @@ #include #include #include +#include +#include +#include +#include #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() { : " ● Синхронизирован"; 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(); diff --git a/views/bank/AccountSettingsWindow.h b/views/bank/AccountSettingsWindow.h index 015dd9a..d292cee 100644 --- a/views/bank/AccountSettingsWindow.h +++ b/views/bank/AccountSettingsWindow.h @@ -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; diff --git a/views/device/DeviceSettingsWindow.cpp b/views/device/DeviceSettingsWindow.cpp index c01552d..5df5f20 100644 --- a/views/device/DeviceSettingsWindow.cpp +++ b/views/device/DeviceSettingsWindow.cpp @@ -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);