703 lines
27 KiB
C++
703 lines
27 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() {
|
||
// 1. Прямое совпадение по тексту заголовка
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Введите код доступа")) ||
|
||
node.contentDesc.contains(QString::fromUtf8("Введите ПИН")) ||
|
||
node.contentDesc.contains(QString::fromUtf8("код доступа")) ||
|
||
node.text.contains(QString::fromUtf8("код доступа"))) {
|
||
return true;
|
||
}
|
||
}
|
||
// 2. Fallback: если на экране есть полная цифровая клавиатура 0-9
|
||
// (content-desc каждой кнопки = одна цифра) — это PIN-экран.
|
||
int digitButtons = 0;
|
||
bool seen[10] = {false};
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.size() != 1) continue;
|
||
const QChar c = node.contentDesc.at(0);
|
||
if (!c.isDigit()) continue;
|
||
const int d = c.digitValue();
|
||
if (d < 0 || d > 9 || seen[d]) continue;
|
||
seen[d] = true;
|
||
++digitButtons;
|
||
}
|
||
return digitButtons >= 10;
|
||
}
|
||
|
||
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 hasBottomNav = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Главная"))) {
|
||
hasGlavnaya = true;
|
||
}
|
||
// Нижняя навигация — любая из соседних вкладок указывает, что мы на
|
||
// основной странице приложения, а не на splash/PIN/диалоге.
|
||
if (node.contentDesc == QString::fromUtf8("Переводы") ||
|
||
node.contentDesc == QString::fromUtf8("Сервисы") ||
|
||
node.contentDesc == QString::fromUtf8("MiniApps") ||
|
||
node.contentDesc == QString::fromUtf8("История")) {
|
||
hasBottomNav = true;
|
||
}
|
||
if (hasGlavnaya && hasBottomNav) {
|
||
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 {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findRefreshButtonOnHomeScreen() {
|
||
// В блоке content-desc="TJS\n<дата>" три clickable дочерних элемента:
|
||
// View с балансом, Button (глазик) и ImageView (рефреш). Тапать нужно
|
||
// именно ImageView — он правее глазика и единственный ImageView внутри
|
||
// bounds parent'а.
|
||
Node parent;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.startsWith("TJS") && node.contentDesc.contains('\n')) {
|
||
parent = node;
|
||
break;
|
||
}
|
||
}
|
||
if (parent.x1() < 0) return {};
|
||
|
||
Node best;
|
||
for (const Node &node : m_nodes) {
|
||
if (!node.clickable) continue;
|
||
if (node.className != "android.widget.ImageView") continue;
|
||
if (node.x1() < parent.x1() || node.x2() > parent.x2()) continue;
|
||
if (node.y1() < parent.y1() || node.y2() > parent.y2()) continue;
|
||
if (best.x1() < 0 || node.x1() > best.x1()) {
|
||
best = node;
|
||
}
|
||
}
|
||
return best;
|
||
}
|
||
|
||
bool ScreenXmlParser::isSideMenu() {
|
||
bool hasVyhod = false;
|
||
bool hasIdentified = false;
|
||
for (const Node &node : m_nodes) {
|
||
// До 3.2.33 кнопка была "Выход", с 3.2.33 — "Выйти".
|
||
if (node.contentDesc == QString::fromUtf8("Выход") ||
|
||
node.contentDesc == QString::fromUtf8("Выйти")) {
|
||
hasVyhod = true;
|
||
}
|
||
// До 3.2.33: "Идентифицирован\nУспешно" — отдельная нода (startsWith).
|
||
// С 3.2.33: "Идентифицирован" вшито в блок профиля
|
||
// ("IN\nIsroil Nazarov\n+992 ...\nИдентифицирован\nУспешно") — нужен contains.
|
||
if (node.contentDesc.contains(QString::fromUtf8("Идентифицирован"))) {
|
||
hasIdentified = true;
|
||
}
|
||
if (hasVyhod && hasIdentified) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfileFullName() {
|
||
// До 3.2.33 одна нода содержит:
|
||
// "Имя Фамилия\n+992 (18) 555-5519\nВерсия: 3.2.5\nСборка: 390:3443"
|
||
// С 3.2.33 "Версия"/"Сборка" вынесены в отдельные ноды, а профиль:
|
||
// "IN\nIsroil Nazarov\n+992 (17) 682-7668\nИдентифицирован\nУспешно"
|
||
// Находим ноду по наличию "Идентифицирован" ИЛИ "Версия:+Сборка:", затем
|
||
// выбираем строку, идущую непосредственно перед строкой с телефоном "+...".
|
||
static const QRegularExpression phoneRe(R"(^\+\d)");
|
||
for (const Node &node : m_nodes) {
|
||
const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован"));
|
||
const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||
node.contentDesc.contains(QString::fromUtf8("Сборка:"));
|
||
if (!isNewProfileBlock && !isOldProfileBlock) continue;
|
||
const QStringList lines = node.contentDesc.split('\n');
|
||
for (int i = 0; i < lines.size(); ++i) {
|
||
if (phoneRe.match(lines[i].trimmed()).hasMatch() && i > 0) {
|
||
return lines[i - 1].trimmed();
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfilePhone() {
|
||
static const QRegularExpression phoneRe(R"(^\+\d)");
|
||
for (const Node &node : m_nodes) {
|
||
const bool isNewProfileBlock = node.contentDesc.contains(QString::fromUtf8("Идентифицирован"));
|
||
const bool isOldProfileBlock = node.contentDesc.contains(QString::fromUtf8("Версия:")) &&
|
||
node.contentDesc.contains(QString::fromUtf8("Сборка:"));
|
||
if (!isNewProfileBlock && !isOldProfileBlock) continue;
|
||
for (const QString &line : node.contentDesc.split('\n')) {
|
||
const QString trimmed = line.trimmed();
|
||
if (phoneRe.match(trimmed).hasMatch()) {
|
||
return trimmed;
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QMap<QString, QString> ScreenXmlParser::parseLabelValuePairs() {
|
||
// Применяется к экранам "Платеж выполнен" и подобным, где с 3.2.33
|
||
// лейблы и значения сидят в отдельных View-нодах: лейбл слева
|
||
// (content-desc оканчивается ':'), значение — справа на той же Y-полосе.
|
||
QMap<QString, QString> result;
|
||
for (int i = 0; i < m_nodes.size(); ++i) {
|
||
const QString rawLabel = m_nodes[i].contentDesc.trimmed();
|
||
if (rawLabel.isEmpty() || !rawLabel.endsWith(':')) continue;
|
||
const QString label = rawLabel.left(rawLabel.size() - 1).trimmed();
|
||
if (label.isEmpty() || result.contains(label)) continue;
|
||
|
||
const Node &lbl = m_nodes[i];
|
||
int bestIdx = -1;
|
||
for (int j = 0; j < m_nodes.size(); ++j) {
|
||
if (j == i) continue;
|
||
const QString candidate = m_nodes[j].contentDesc.trimmed();
|
||
if (candidate.isEmpty()) continue;
|
||
if (candidate.endsWith(':')) continue;
|
||
if (m_nodes[j].x1() < lbl.x2()) continue;
|
||
if (qAbs(m_nodes[j].y1() - lbl.y1()) > 8) continue;
|
||
if (bestIdx < 0 || m_nodes[j].x1() < m_nodes[bestIdx].x1()) {
|
||
bestIdx = j;
|
||
}
|
||
}
|
||
if (bestIdx >= 0) {
|
||
result[label] = m_nodes[bestIdx].contentDesc.trimmed();
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
|
||
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.clickable) continue;
|
||
if (!node.contentDesc.contains("****")) continue;
|
||
if (node.contentDesc.contains(QString::fromUtf8("Кредитная карта"))) 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 {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findCardSettingsButton() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.clickable && node.contentDesc == QString::fromUtf8("Настройки карты")) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isCardSettingsBottomSheet() {
|
||
bool hasRequisites = false;
|
||
bool hasBlock = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasRequisites = true;
|
||
if (node.contentDesc == QString::fromUtf8("Заблокировать карту")) hasBlock = true;
|
||
if (hasRequisites && hasBlock) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
Node ScreenXmlParser::findCardRequisitesMenuItem() {
|
||
// На bottom sheet это ImageView; на экране "Реквизиты и информация"
|
||
// одноимённый таб — Button. Берём именно ImageView, чтобы не перепутать.
|
||
for (const Node &node : m_nodes) {
|
||
if (node.clickable
|
||
&& node.className == "android.widget.ImageView"
|
||
&& node.contentDesc == QString::fromUtf8("Реквизиты карты")) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isCardRequisitesScreen() {
|
||
bool hasCardTab = false;
|
||
bool hasAccountTab = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className != "android.widget.Button") continue;
|
||
if (node.contentDesc == QString::fromUtf8("Реквизиты карты")) hasCardTab = true;
|
||
if (node.contentDesc == QString::fromUtf8("Реквизиты счета")) hasAccountTab = true;
|
||
if (hasCardTab && hasAccountTab) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
Node ScreenXmlParser::findAccountRequisitesTab() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.clickable
|
||
&& node.className == "android.widget.Button"
|
||
&& node.contentDesc == QString::fromUtf8("Реквизиты счета")) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseAccountNumber() {
|
||
// Берём ноду "Номер счета" (label) и ищем ближайший узел снизу
|
||
// с чисто цифровым content-desc — это и есть номер счёта.
|
||
Node label;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Номер счета")) {
|
||
label = node;
|
||
break;
|
||
}
|
||
}
|
||
if (label.y2() < 0) return {};
|
||
|
||
static const QRegularExpression digitsOnly(R"(^\d{10,}$)");
|
||
Node best;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.y1() < label.y2()) continue;
|
||
if (!digitsOnly.match(node.contentDesc).hasMatch()) continue;
|
||
if (best.y1() < 0 || node.y1() < best.y1()) {
|
||
best = node;
|
||
}
|
||
}
|
||
return best.contentDesc;
|
||
}
|
||
|
||
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 {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findSumEditText() {
|
||
// На экране перевода по карте порядок EditText'ов сверху вниз:
|
||
// [0] номер карты, [1] сумма, [2] комментарий.
|
||
QList<Node> editTexts;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className == "android.widget.EditText") editTexts.append(node);
|
||
}
|
||
std::sort(editTexts.begin(), editTexts.end(),
|
||
[](const Node &a, const Node &b) { return a.y1() < b.y1(); });
|
||
return editTexts.size() >= 2 ? editTexts[1] : Node{};
|
||
}
|
||
|
||
Node ScreenXmlParser::tryToFindSystemPermissionDenyButton() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.resourceId == "com.android.permissioncontroller:id/permission_deny_button"
|
||
&& node.clickable) {
|
||
return node;
|
||
}
|
||
}
|
||
for (const Node &node : m_nodes) {
|
||
if (node.clickable && node.text.trimmed() == QString::fromUtf8("ЗАПРЕТИТЬ")) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::tryToFindGooglePlayBanner(const int screenWidth, const int screenHeight) {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains("Закрыть диалоговое окно") && node.clickable) {
|
||
return node;
|
||
}
|
||
}
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains("Доступно обновление")) {
|
||
Node tapNode;
|
||
const int cx = screenWidth / 2;
|
||
const int cy = screenHeight / 20;
|
||
tapNode.setBounds(cx, cy, cx + 1, cy + 1);
|
||
return tapNode;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
} // namespace Dushanbe
|