478 lines
16 KiB
C++
478 lines
16 KiB
C++
#include "ScreenXmlParser.h"
|
||
|
||
#include <QRegularExpression>
|
||
#include <QXmlStreamReader>
|
||
#include <QDomDocument>
|
||
|
||
#include "android/xml/Node.h"
|
||
|
||
namespace Dushanbe {
|
||
|
||
static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])");
|
||
|
||
ScreenXmlParser::ScreenXmlParser(QObject *parent) : QObject(parent) {
|
||
}
|
||
|
||
ScreenXmlParser::~ScreenXmlParser() = default;
|
||
|
||
void ScreenXmlParser::parseAndSaveXml(const QString &xml) {
|
||
QDomDocument doc;
|
||
doc.setContent(xml);
|
||
m_xml = doc.documentElement();
|
||
m_nodes.clear();
|
||
|
||
QXmlStreamReader reader(xml);
|
||
while (!reader.atEnd()) {
|
||
reader.readNext();
|
||
if (reader.isStartElement() && reader.name() == u"node") {
|
||
Node node;
|
||
if (reader.attributes().hasAttribute("bounds")) {
|
||
QString bounds = reader.attributes().value("bounds").toString();
|
||
if (QRegularExpressionMatch match = boundsRegex.match(bounds); match.hasMatch()) {
|
||
node.setBounds(
|
||
match.captured(1).toInt(),
|
||
match.captured(2).toInt(),
|
||
match.captured(3).toInt(),
|
||
match.captured(4).toInt()
|
||
);
|
||
}
|
||
}
|
||
if (reader.attributes().hasAttribute("text")) {
|
||
node.text = reader.attributes().value("text").toString();
|
||
}
|
||
if (reader.attributes().hasAttribute("content-desc")) {
|
||
node.contentDesc = reader.attributes().value("content-desc").toString();
|
||
}
|
||
if (reader.attributes().hasAttribute("class")) {
|
||
node.className = reader.attributes().value("class").toString();
|
||
}
|
||
if (reader.attributes().hasAttribute("clickable")) {
|
||
node.clickable = reader.attributes().value("clickable").toString() == "true";
|
||
}
|
||
if (reader.attributes().hasAttribute("focusable")) {
|
||
node.focusable = reader.attributes().value("focusable").toString() == "true";
|
||
}
|
||
if (reader.attributes().hasAttribute("resource-id")) {
|
||
node.resourceId = reader.attributes().value("resource-id").toString();
|
||
}
|
||
if (reader.attributes().hasAttribute("hint")) {
|
||
node.hint = reader.attributes().value("hint").toString();
|
||
}
|
||
if (reader.attributes().hasAttribute("password")) {
|
||
node.password = reader.attributes().value("password").toString() == "true";
|
||
}
|
||
m_nodes.append(node);
|
||
}
|
||
}
|
||
}
|
||
|
||
const QList<Node> &ScreenXmlParser::findAllNodes() const {
|
||
return m_nodes;
|
||
}
|
||
|
||
Node ScreenXmlParser::findTextNode(const QString &text) {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.text.trimmed() == text) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc) {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == contentDesc) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findNodeByContentDesc(const QString &contentDesc, bool contains) {
|
||
for (const Node &node : m_nodes) {
|
||
if (contains) {
|
||
if (node.contentDesc.contains(contentDesc)) {
|
||
return node;
|
||
}
|
||
} else {
|
||
if (node.contentDesc == contentDesc) {
|
||
return node;
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findButtonNode(const QString &text, bool contains) {
|
||
for (const Node &node : m_nodes) {
|
||
if (!node.clickable) continue;
|
||
if (contains) {
|
||
if (node.text.contains(text) || node.contentDesc.contains(text)) {
|
||
return node;
|
||
}
|
||
} else {
|
||
if (node.text.trimmed() == text || node.contentDesc == text) {
|
||
return node;
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findFirstEditText() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className == "android.widget.EditText") {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isPinCodeScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Введите код доступа"))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
Node ScreenXmlParser::findPinCodeEditText() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className == "android.widget.EditText" && node.password) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isHomeScreen() {
|
||
bool hasGlavnaya = false;
|
||
bool hasTJS = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Главная")) {
|
||
hasGlavnaya = true;
|
||
}
|
||
if (node.contentDesc.contains("TJS")) {
|
||
hasTJS = true;
|
||
}
|
||
if (hasGlavnaya && hasTJS) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
double ScreenXmlParser::parseBalanceFromHomeScreen() {
|
||
// На домашнем экране: parent node content-desc="TJS\n03.04.26 - 21:46:15"
|
||
// внутри него child node content-desc="0.00" (или "10.00")
|
||
// Стратегия: находим индекс parent, затем child сразу после него с числовым content-desc
|
||
for (int i = 0; i < m_nodes.size(); ++i) {
|
||
if (!m_nodes[i].contentDesc.startsWith("TJS")) continue;
|
||
// Проверяем что это блок с датой (не просто "TJS" текст)
|
||
if (!m_nodes[i].contentDesc.contains('\n')) continue;
|
||
|
||
// Child node с балансом идёт сразу после parent или рядом
|
||
for (int j = i + 1; j < qMin(i + 5, m_nodes.size()); ++j) {
|
||
QString raw = m_nodes[j].contentDesc.trimmed();
|
||
if (raw.isEmpty()) continue;
|
||
raw.replace(',', '.');
|
||
raw.remove(' ');
|
||
bool ok = false;
|
||
double val = raw.toDouble(&ok);
|
||
if (ok) return val;
|
||
}
|
||
}
|
||
return 0.0;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseUserNameFromHomeScreen() {
|
||
// TODO: реализовать парсинг имени
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isSideMenu() {
|
||
bool hasVyhod = false;
|
||
bool hasIdentified = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Выход")) {
|
||
hasVyhod = true;
|
||
}
|
||
if (node.contentDesc == QString::fromUtf8("Идентифицирован")) {
|
||
hasIdentified = true;
|
||
}
|
||
if (hasVyhod && hasIdentified) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfileFullName() {
|
||
// content-desc: "Мехрубон Имомкулзода\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443"
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
|
||
QStringList lines = node.contentDesc.split('\n');
|
||
if (!lines.isEmpty()) {
|
||
return lines[0].trimmed();
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfilePhone() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||
node.contentDesc.contains(QString::fromUtf8("Сборка:"))) {
|
||
QStringList lines = node.contentDesc.split('\n');
|
||
if (lines.size() >= 2) {
|
||
return lines[1].trimmed();
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isCardsScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Мои карты")) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QList<ScreenXmlParser::CardInfo> ScreenXmlParser::parseCards() {
|
||
// Карты — ImageView с content-desc формата:
|
||
// "Обновлено: 03.04.26 - 22:43:57\n0.00 TJS\n**** 3258\n до \n11/35\nIMOMKULZODA MEKHRUBON"
|
||
// или "-\n**** \n до \n12/29\n \nКредитная карта"
|
||
QList<CardInfo> result;
|
||
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className != "android.widget.ImageView") continue;
|
||
if (!node.contentDesc.contains("****")) continue;
|
||
|
||
QStringList lines;
|
||
for (const QString &line : node.contentDesc.split('\n')) {
|
||
QString trimmed = line.trimmed();
|
||
if (!trimmed.isEmpty()) {
|
||
lines.append(trimmed);
|
||
}
|
||
}
|
||
|
||
CardInfo card;
|
||
|
||
for (const QString &line : lines) {
|
||
// Баланс: "0.00 TJS"
|
||
if (line.contains("TJS")) {
|
||
QStringList parts = line.split(' ');
|
||
if (parts.size() >= 2) {
|
||
card.amount = parts[0].toDouble();
|
||
card.currency = "TJS";
|
||
}
|
||
continue;
|
||
}
|
||
// Последние цифры: "**** 3258"
|
||
if (line.startsWith("****")) {
|
||
QString nums = line.mid(4).trimmed();
|
||
if (!nums.isEmpty() && nums[0].isDigit()) {
|
||
card.lastNumbers = nums;
|
||
}
|
||
continue;
|
||
}
|
||
// Срок: "11/35" или "12/29"
|
||
static const QRegularExpression expiryRe(R"(^\d{2}/\d{2}$)");
|
||
if (expiryRe.match(line).hasMatch()) {
|
||
card.expiry = line;
|
||
continue;
|
||
}
|
||
// Пропускаем "до", "Обновлено:...", "-"
|
||
if (line == QString::fromUtf8("до") || line.startsWith(QString::fromUtf8("Обновлено")) || line == "-") {
|
||
continue;
|
||
}
|
||
// Имя держателя (CAPS) или описание
|
||
if (line == line.toUpper() && line.length() > 3) {
|
||
card.holderName = line;
|
||
} else {
|
||
card.description = line;
|
||
}
|
||
}
|
||
|
||
result.append(card);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
bool ScreenXmlParser::isCardDetailScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Карта")) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseFullCardNumber() {
|
||
// Полный номер карты: "9762 0002 0711 3258" — clickable View
|
||
static const QRegularExpression cardNumberRe(R"(^\d{4}\s\d{4}\s\d{4}\s\d{4}$)");
|
||
for (const Node &node : m_nodes) {
|
||
if (cardNumberRe.match(node.contentDesc).hasMatch()) {
|
||
return node.contentDesc;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
double ScreenXmlParser::parseCardDetailBalance() {
|
||
// "0,00 TJS" или "- TJS" — содержит TJS, но не "сомони"
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains("TJS") && !node.contentDesc.contains(QString::fromUtf8("сомони"))) {
|
||
QString raw = node.contentDesc.trimmed();
|
||
raw.remove("TJS");
|
||
raw = raw.trimmed();
|
||
if (raw == "-" || raw.isEmpty()) return 0.0;
|
||
raw.replace(',', '.');
|
||
raw.remove(' ');
|
||
bool ok = false;
|
||
double val = raw.toDouble(&ok);
|
||
if (ok) return val;
|
||
}
|
||
}
|
||
return 0.0;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseCardDetailHolder() {
|
||
// Имя держателя — текст в CAPS, не содержит цифр, длина > 3
|
||
static const QRegularExpression cardNumberRe(R"(\d{4}\s\d{4})");
|
||
for (const Node &node : m_nodes) {
|
||
const QString &desc = node.contentDesc;
|
||
if (desc.isEmpty() || desc.length() < 4) continue;
|
||
if (desc == desc.toUpper() && !desc.contains(QRegularExpression(R"(\d)")) &&
|
||
desc != QString::fromUtf8("TJS")) {
|
||
return desc.trimmed();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isHistoryScreen() {
|
||
bool hasOperacii = false;
|
||
bool hasVypiska = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Операции")) {
|
||
hasOperacii = true;
|
||
}
|
||
if (node.contentDesc == QString::fromUtf8("Выписка")) {
|
||
hasVypiska = true;
|
||
}
|
||
if (hasOperacii && hasVypiska) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QList<ScreenXmlParser::TransactionItem> ScreenXmlParser::parseTransactionItems() {
|
||
// Транзакции — ImageView с content-desc формата:
|
||
// "9762***3258\n992882770011\n493.00 \nDC (по номеру телефона)\n 18:57:10"
|
||
QList<TransactionItem> result;
|
||
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className != "android.widget.ImageView") continue;
|
||
if (!node.contentDesc.contains("***")) continue;
|
||
|
||
QStringList lines;
|
||
for (const QString &line : node.contentDesc.split('\n')) {
|
||
QString trimmed = line.trimmed();
|
||
if (!trimmed.isEmpty()) {
|
||
lines.append(trimmed);
|
||
}
|
||
}
|
||
|
||
if (lines.size() < 4) continue;
|
||
|
||
TransactionItem tx;
|
||
tx.cardMask = lines[0]; // "9762***3258"
|
||
tx.phone = lines[1]; // "992882770011"
|
||
|
||
// Сумма: "493.00" (может быть с пробелом в конце)
|
||
bool ok = false;
|
||
tx.amount = lines[2].toDouble(&ok);
|
||
if (!ok) tx.amount = 0.0;
|
||
|
||
tx.description = lines[3]; // "DC (по номеру телефона)" или "Tcell"
|
||
|
||
// Время — последний элемент
|
||
if (lines.size() >= 5) {
|
||
tx.time = lines[4]; // "18:57:10"
|
||
}
|
||
|
||
result.append(tx);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
bool ScreenXmlParser::isTransactionDetailScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Дата операции:")) &&
|
||
node.contentDesc.contains(QString::fromUtf8("Номер операции:"))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
ScreenXmlParser::TransactionDetail ScreenXmlParser::parseTransactionDetail() {
|
||
// Все данные в одном content-desc, формат "ключ: \nзначение"
|
||
TransactionDetail detail;
|
||
|
||
for (const Node &node : m_nodes) {
|
||
if (!node.contentDesc.contains(QString::fromUtf8("Дата операции:"))) continue;
|
||
|
||
// Разбиваем по \n, парсим пары "ключ:" + следующая строка = значение
|
||
QStringList lines = node.contentDesc.split('\n');
|
||
|
||
for (int i = 0; i < lines.size(); ++i) {
|
||
const QString line = lines[i].trimmed();
|
||
|
||
if (line.startsWith(QString::fromUtf8("Дата операции:")) && i + 1 < lines.size()) {
|
||
detail.date = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Время операции:")) && i + 1 < lines.size()) {
|
||
detail.time = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Номер операции:")) && i + 1 < lines.size()) {
|
||
detail.operationId = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Поставщик:")) && i + 1 < lines.size()) {
|
||
detail.provider = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Счет отправителя:")) && i + 1 < lines.size()) {
|
||
detail.senderAccount = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Счет получателя:")) && i + 1 < lines.size()) {
|
||
detail.receiverAccount = lines[i + 1].trimmed();
|
||
} else if (line.startsWith(QString::fromUtf8("Сумма операции:")) && i + 1 < lines.size()) {
|
||
detail.amount = lines[i + 1].trimmed().toDouble();
|
||
} else if (line.startsWith(QString::fromUtf8("Комиссия:")) && i + 1 < lines.size()) {
|
||
detail.fee = lines[i + 1].trimmed().toDouble();
|
||
} else if (line.startsWith(QString::fromUtf8("Статус:")) && i + 1 < lines.size()) {
|
||
detail.status = lines[i + 1].trimmed();
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
|
||
return detail;
|
||
}
|
||
|
||
Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className == "android.widget.EditText" && node.hint.contains(hint)) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
} // namespace Dushanbe
|