538 lines
18 KiB
C++
538 lines
18 KiB
C++
#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);
|
||
// ЕСПП
|
||
}
|