1
0
forked from BRT/arc
arc/android/toss/ScreenXmlTossParser.cpp
2026-02-22 20:48:16 +07:00

538 lines
18 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ScreenXmlTossParser.h"
#include <QRegularExpression>
#include <QMap>
#include <QSet>
#include <QList>
#include <QXmlStreamReader>
#include <QDomDocument>
#include <QFile>
#include <QDateTime>
#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<Node> ScreenXmlTossParser::getNodeNumbers(double value) {
// Формат 'f' с precision = 0 — дробная часть отбрасывается
QString str = QString::number(value, 'f', 0);
// Разбиваем на отдельные символы
QList<Node> 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<Node> ScreenXmlTossParser::findPinCodeNodes(const QString &pinCode) {
QMap<QString, Node> buttons;
const QSet<QString> 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<Node> 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<QDomElement> findElementsWithPaymentsToss(const QDomElement &parent, const QString &attribute,
const QString &value) {
QList<QDomElement> 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<TransactionInfo> ScreenXmlTossParser::parsePositiveTransactions() {
QList<TransactionInfo> 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<QString, double> 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<int>(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<AccountInfo> ScreenXmlTossParser::parseAccountsInfo() {
const QDomNodeList nodes = m_xml.elementsByTagName("node");
QList<AccountInfo> 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);
// ЕСПП
}