#include "ScreenXmlTossParser.h" #include #include #include #include #include #include #include #include #include "adb/AdbUtils.h" #include "android/xml/Node.h" #include "db/AccountInfo.h" #include "db/TransactionInfo.h" #include "time/DateUtils.h" ScreenXmlTossParser::ScreenXmlTossParser(QObject *parent) : QObject(parent) { } ScreenXmlTossParser::~ScreenXmlTossParser() = default; static const QRegularExpression boundsRegex(R"(\[([0-9]+),([0-9]+)\]\[([0-9]+),([0-9]+)\])"); Node ScreenXmlTossParser::findTextNode(const QString &text) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.trimmed() == text) { return node; } } return {}; } Node ScreenXmlTossParser::findTextContainsNode(const QString &text) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.contains(text)) { return node; } } return {}; } Node ScreenXmlTossParser::findTextNode(const QString &text, const int x1, const int y1, const int x2, const int y2) { for (const Node &node: m_nodes) { if (node.className == "android.widget.TextView" && node.text.trimmed() == text && node.x() >= x1 && node.x() <= x2 && node.y() >= y1 && node.y() <= y2) { return node; } } return {}; } Node ScreenXmlTossParser::findEditText(const QString &text) { for (const Node &node: m_nodes) { if (node.className == "android.widget.EditText" && node.text == text) { return node; } } return {}; } Node ScreenXmlTossParser::findEditTextByResourceId(const QString &resourceId) { for (const Node &node: m_nodes) { if (node.className == "android.widget.EditText" && node.resourceId == resourceId) { return node; } } return {}; } QList ScreenXmlTossParser::getNodeNumbers(double value) { // Формат 'f' с precision = 0 — дробная часть отбрасывается QString str = QString::number(value, 'f', 0); // Разбиваем на отдельные символы QList nodes; for (QChar ch: str) { QString num = QString(ch); for (const Node &node: m_nodes) { if (node.contentDesc == num && node.className == "android.widget.Button") { nodes.append(node); } } } return nodes; } Node ScreenXmlTossParser::findNodeByClassName(const QString &className) { for (const Node &node: m_nodes) { if (node.className == className) { return node; } } return {}; } Node ScreenXmlTossParser::findButtonNode(const QString &text, const bool contains) { for (const Node &node: m_nodes) { if (contains) { if (node.className == "android.widget.Button" && node.text.contains(text)) { return node; } } else { if (node.className == "android.widget.Button" && node.text.trimmed() == text) { return node; } } } return {}; } bool ScreenXmlTossParser::isHomeScreen() { Node activitiesBtn; for (const Node &node: m_nodes) { if (node.text.contains("Transfer") && node.resourceId == "viva.republica.toss:id/rightButton") { activitiesBtn = node; } } return !activitiesBtn.isEmpty(); } QList ScreenXmlTossParser::findPinCodeNodes(const QString &pinCode) { QMap buttons; const QSet numberSet = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"}; for (const Node &node: m_nodes) { const QString &text = node.text.trimmed(); if (node.className == "android.widget.Button" && numberSet.contains(text)) { buttons[text] = node; } } QList nodes; for (int i = 0; i < pinCode.length(); ++i) { nodes.append(buttons[QString(pinCode.at(i))]); } return nodes; } Node convertTossDomElementToNode(const QDomElement &element) { auto node = Node(); if (element.hasAttribute("bounds")) { const QString bounds = element.attribute("bounds"); if (const 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 (element.hasAttribute("text")) { node.text = element.attribute("text"); } if (element.hasAttribute("content-desc")) { node.contentDesc = element.attribute("content-desc"); } if (element.hasAttribute("class")) { node.className = element.attribute("class"); } if (element.hasAttribute("clickable")) { node.clickable = element.attribute("clickable") == "true"; } if (element.hasAttribute("focusable")) { node.focusable = element.attribute("focusable") == "true"; } if (element.hasAttribute("resource-id")) { node.resourceId = element.attribute("resource-id"); } return node; } void ScreenXmlTossParser::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() == "node") { 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(); } m_nodes.append(node); } } } QDomElement findSubElementToss(const QDomElement &elem, const QString &index) { if (elem.hasChildNodes()) { QDomNodeList subInfoList = elem.childNodes(); for (QDomNode subInfoNode: subInfoList) { if (!subInfoNode.isElement()) continue; QDomElement subInfo = subInfoNode.toElement(); if (subInfo.attribute("index") == index) { return subInfo; } } } return {}; } QString findSubInfoToss(const QDomElement &elem, const QString &index) { QDomElement subInfo = findSubElementToss(elem, index); if (!subInfo.isNull()) { return subInfo.attribute("text"); } return ""; } QDomNode findNodeToss(const QDomNode &node, const QString &text) { if (node.isElement()) { QString nodeText = node.toElement().attribute("text"); if (nodeText.toLower().startsWith(text)) { return node; } } QDomNode child = node.firstChild(); while (!child.isNull()) { QDomNode result = findNodeToss(child, text); if (!result.isNull()) { return result; } child = child.nextSibling(); } return {}; } QList findElementsWithPaymentsToss(const QDomElement &parent, const QString &attribute, const QString &value) { QList result; QDomElement element = parent.firstChildElement(); while (!element.isNull()) { // Проверка самого element // if (element.hasAttribute(attribute) && element.attribute(attribute) == value) { // result.append(element); // } // Проверка его дочерних элементов QDomElement child = element.firstChildElement(); while (!child.isNull()) { if (child.hasAttribute(attribute) && child.attribute(attribute) == value) { result.append(child); } child = child.nextSiblingElement(); } // Рекурсивный обход result.append(findElementsWithPaymentsToss(element, attribute, value)); element = element.nextSiblingElement(); } return result; } QDomElement findElementWithPaymentsToss(const QDomElement &parent, const QString &attribute, const QString &value) { QDomElement element = parent.firstChildElement(); while (!element.isNull()) { QDomElement child = element.firstChildElement(); while (!child.isNull()) { if (child.hasAttribute(attribute) && child.attribute(attribute) == value) { return child; } child = child.nextSiblingElement(); } QDomElement found = findElementWithPaymentsToss(element, attribute, value); if (!found.isNull()) { return found; } element = element.nextSiblingElement(); } return {}; } QDateTime getUtcFromSeoulDate(const QString &date, const QString &time) { int year = QDate::currentDate().year(); // или укажи вручную нужный год QString withYear = QString("%1년 %2 %3").arg(year).arg(date).arg(time); // "2025년 7월 28일 17:54" QLocale korean(QLocale::Korean, QLocale::SouthKorea); QString format = "yyyy년 M월 d일 H:mm"; QDateTime dt = korean.toDateTime(withYear, format); if (dt.isValid()) { dt.setTimeZone(QTimeZone("Asia/Seoul")); return dt.toTimeZone(QTimeZone::utc()); } else { qWarning() << "Ошибка разбора даты:" << withYear; return {}; } } Node ScreenXmlTossParser::findFirstAccountLine() { Node node; auto elements = findElementsWithPaymentsToss(m_xml, "text", "Transfer"); if (elements.length() == 2) { for (QDomElement &element: elements) { qDebug() << "Found account: " << element.attribute("content-desc"); } } if (!elements.isEmpty()) { node = convertTossDomElementToNode(elements[0].parentNode().toElement()); } return node; } QList ScreenXmlTossParser::parsePositiveTransactions() { QList transactions; QDomElement child = findElementWithPaymentsToss(m_xml, "class", "android.widget.ListView"); QDomNodeList listView = child.childNodes(); qDebug() << listView.count(); if (!listView.isEmpty()) { QString date = ""; for (QDomNode node: listView) { QString title = node.toElement().attribute("text"); if (title == "전체" || title.startsWith("AD") || title.endsWith("AD")) { continue; } if (!title.isEmpty()) { date = node.toElement().attribute("text"); } if (date.isEmpty()) { continue; } const QDomNodeList trBoxGroup = node.childNodes(); if (trBoxGroup.count() > 0) { TransactionInfo transaction; for (QDomNode item: trBoxGroup) { QDomElement groupItem = item.toElement(); QString line = groupItem.attribute("text"); if (line.contains("잔액") && line.contains("입금")) { QString beforeBalance = line.split("잔액").first().trimmed(); QString time = beforeBalance.split("입금").first().trimmed().last(5); QString desc = beforeBalance.split("입금").first().replace(time, "").trimmed(); QString amount = beforeBalance.split("입금").last().replace("원", "").replace(",", "").trimmed(); transaction.amount = amount.toDouble(); transaction.description = desc; transaction.bankTime = date + " " + time; transaction.completeTime = getUtcFromSeoulDate(date, time); } } if (transaction.amount > 0) { transactions.append(transaction); } } } } else { qDebug() << "ListView is empty"; } return transactions; } float extractKoreanWonFromText(const QString &input) { QRegularExpression re(R"(₩([\d,]+))"); QRegularExpressionMatch match = re.match(input); if (!match.hasMatch()) { qWarning() << "Не удалось найти сумму в строке:" << input; return 0.0f; } QString numberStr = match.captured(1); numberStr.remove(','); // удаляем запятые return numberStr.toFloat(); } double parseKoreanNumber(const QString &input) { // Карта корейских единиц в числовые множители static const QMap unitMap = { {QStringLiteral("조"), 1e12}, {QStringLiteral("억"), 1e8}, {QStringLiteral("만"), 1e4} }; // Статические регулярки (Clazy: don't create temporaries) static const QRegularExpression cleanRe(QStringLiteral("[^0-9.,만억조]")); static const QRegularExpression segmentRe(QStringLiteral(R"(([\d,]+(?:\.\d+)?)(조|억|만))")); QString s = input; s.remove(cleanRe); double total = 0.0; auto it = segmentRe.globalMatch(s); qsizetype pos = 0; while (it.hasNext()) { auto match = it.next(); QString numStr = match.captured(1); QString unitStr = match.captured(2); numStr.remove(','); bool ok = false; const double num = numStr.toDouble(&ok); if (ok) total += num * unitMap.value(unitStr); pos = match.capturedEnd(); } // Обработка остатка (обычное число, возможно с дробной частью) QString rem = s.mid(static_cast(pos)); rem.remove(','); if (!rem.isEmpty()) { bool ok = false; const double num = rem.toDouble(&ok); if (ok) total += num; } return total; } double ScreenXmlTossParser::parseAccountAmount() { const QDomNodeList nodes = m_xml.elementsByTagName("node"); for (int i = 0; i < nodes.count(); ++i) { const QDomElement el = nodes.at(i).toElement(); // Аккаунтов может быть несколько, вот берем первый по списку if (el.attribute("text") == "Top up" && el.attribute("class") == "android.widget.Button") { for (QDomNode node: el.parentNode().childNodes()) { const QDomElement child = node.toElement(); if (child.attribute("text").contains("만") || child.attribute("text").contains("원")) { return parseKoreanNumber(child.attribute("text")); } } } } return 0; } QList ScreenXmlTossParser::parseAccountsInfo() { const QDomNodeList nodes = m_xml.elementsByTagName("node"); QList accounts; for (int i = 0; i < nodes.count(); ++i) { const QDomElement el = nodes.at(i).toElement(); // Аккаунтов может быть несколько, вот берем первый по списку if (el.attribute("text") == "Transfer") { for (QDomNode node: el.parentNode().parentNode().parentNode().childNodes()) { const QDomElement child = node.toElement(); if (child.attribute("content-desc").contains("₩")) { AccountInfo info; info.amount = extractKoreanWonFromText(child.attribute("content-desc")); info.updateTime = DateUtils::getUtcNow(); accounts.append(info); return accounts; } } } // if (el.attribute("content-desc").contains("토스뱅크 통장") && el.attribute("content-desc").contains("₩")) { // AccountInfo info; // info.amount = extractKoreanWonFromText(el.attribute("content-desc")); // info.updateTime = DateUtils::getUtcNow(); // accounts.append(info); // break; // } } return accounts; } void ScreenXmlTossParser::test() { // TransactionInfo info2 = parseExpandedInfo(m_xml); // qDebug().noquote().nospace() << convertTransactionToString(info2); // return; QFile file("assets/rshb/all_products.xml"); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Cannot open file"; } QDomDocument doc; if (!doc.setContent(&file)) { qDebug() << "Failed to parse XML"; } file.close(); qDebug() << "Start parsing XML:"; // Номер карты с последними цифрами QDomElement root = doc.documentElement(); parseAndSaveXml(doc.toString()); // for (const AccountInfo &info: parseAccountsInfo()) { // qDebug().noquote().nospace() << convertAccountToString(info); // } // TransactionInfo info = parseExpandedInfo(root); // qDebug().noquote().nospace() << convertTransactionToString(info); // ЕСПП }