579 lines
18 KiB
C++
579 lines
18 KiB
C++
#include "ScreenXmlParser.h"
|
||
|
||
#include <QMap>
|
||
#include <QRegularExpression>
|
||
#include <QXmlStreamReader>
|
||
#include <QDomDocument>
|
||
|
||
#include "android/xml/Node.h"
|
||
|
||
namespace Black {
|
||
|
||
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("¿Olvidaste tu PIN?"))) {
|
||
return true;
|
||
}
|
||
if (node.contentDesc.contains(QString::fromUtf8("Ingresá tu PIN"))) {
|
||
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 hasSaldo = false;
|
||
bool hasInicio = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Saldo disponible") {
|
||
hasSaldo = true;
|
||
}
|
||
if (node.contentDesc == "Inicio") {
|
||
hasInicio = true;
|
||
}
|
||
if (hasSaldo && hasInicio) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ScreenXmlParser::isSideMenu() {
|
||
bool hasTuInfo = false;
|
||
bool hasCerrar = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Tu Información")) {
|
||
hasTuInfo = true;
|
||
}
|
||
if (node.contentDesc == QString::fromUtf8("Cerrar sesión")) {
|
||
hasCerrar = true;
|
||
}
|
||
if (hasTuInfo && hasCerrar) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
Node ScreenXmlParser::findHolaButton() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.clickable && node.contentDesc.startsWith(QString::fromUtf8("¡Hola!"))) {
|
||
return node;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseUserNameFromHomeScreen() {
|
||
// content-desc: "¡Hola!\nveronica cintia heredia \nvc"
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.startsWith(QString::fromUtf8("¡Hola!"))) {
|
||
QStringList lines = node.contentDesc.split('\n');
|
||
if (lines.size() >= 2) {
|
||
return lines[1].trimmed();
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
double ScreenXmlParser::parseBalanceFromHomeScreen() {
|
||
// Баланс идёт после "Saldo disponible", формат: "$1.857,00" или "$0,00"
|
||
bool afterSaldo = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Saldo disponible") {
|
||
afterSaldo = true;
|
||
continue;
|
||
}
|
||
if (afterSaldo && node.contentDesc.startsWith("$")) {
|
||
// "$1.857,00" → "1857.00"
|
||
QString raw = node.contentDesc.mid(1); // убираем "$"
|
||
raw.remove('.'); // убираем разделитель тысяч
|
||
raw.replace(',', '.'); // запятая → точка
|
||
bool ok = false;
|
||
double val = raw.toDouble(&ok);
|
||
if (ok) return val;
|
||
}
|
||
}
|
||
return 0.0;
|
||
}
|
||
|
||
bool ScreenXmlParser::isProfilePage() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == QString::fromUtf8("Tu información")) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfileNombre() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.hint == "Nombre" && !node.text.isEmpty()) {
|
||
return node.text.trimmed();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfileApellido() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.hint == "Apellido" && !node.text.isEmpty()) {
|
||
return node.text.trimmed();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfileEmail() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.hint == "E-mail" && !node.text.isEmpty()) {
|
||
return node.text.trimmed();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parseProfilePhone() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.hint == QString::fromUtf8("Teléfono celular") && !node.text.isEmpty()) {
|
||
return node.text.trimmed();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isCVUScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "CVU") {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseCVUNumber() {
|
||
// CVU номер идёт после content-desc="CVU", это 22-значный номер
|
||
bool afterCVU = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "CVU") {
|
||
afterCVU = true;
|
||
continue;
|
||
}
|
||
if (afterCVU && !node.contentDesc.isEmpty()) {
|
||
// Проверяем что это числовая строка (CVU — 22 цифры)
|
||
QString cvu = node.contentDesc.trimmed();
|
||
bool allDigits = true;
|
||
for (const QChar &c : cvu) {
|
||
if (!c.isDigit()) {
|
||
allDigits = false;
|
||
break;
|
||
}
|
||
}
|
||
if (allDigits && !cvu.isEmpty()) {
|
||
return cvu;
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findVerMasAfterTuActividad() {
|
||
// "Ver más" идёт сразу после "Tu actividad" на домашнем экране
|
||
bool afterTuActividad = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Tu actividad") {
|
||
afterTuActividad = true;
|
||
continue;
|
||
}
|
||
if (afterTuActividad && node.contentDesc == QString::fromUtf8("Ver más") && node.clickable) {
|
||
return node;
|
||
}
|
||
// Если встретили другую секцию — прекращаем поиск
|
||
if (afterTuActividad && node.contentDesc == "Tarjetas") {
|
||
break;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
bool ScreenXmlParser::isTransactionLoadingScreen() {
|
||
// Экран загрузки: заголовок "Tu Actividad" есть, но транзакций нет
|
||
// Может содержать "Aún no tienes actividad"
|
||
bool hasTuActividad = false;
|
||
bool hasTransferencia = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Tu Actividad") {
|
||
hasTuActividad = true;
|
||
}
|
||
if (node.contentDesc.startsWith("Transferencia")) {
|
||
hasTransferencia = true;
|
||
}
|
||
}
|
||
return hasTuActividad && !hasTransferencia;
|
||
}
|
||
|
||
bool ScreenXmlParser::isTransactionListScreen() {
|
||
// Экран транзакций определяется по заголовку "Tu Actividad" (с большой A)
|
||
// + наличие транзакций (ImageView с "Transferencia")
|
||
// На домашнем экране — "Tu actividad" (с маленькой a)
|
||
bool hasTuActividad = false;
|
||
bool hasTransferencia = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Tu Actividad") {
|
||
hasTuActividad = true;
|
||
}
|
||
if (node.contentDesc.startsWith("Transferencia")) {
|
||
hasTransferencia = true;
|
||
}
|
||
}
|
||
return hasTuActividad && hasTransferencia;
|
||
}
|
||
|
||
QList<TransactionInfo> ScreenXmlParser::parseTransactionItems() {
|
||
// Транзакции — ImageView с content-desc формата:
|
||
// "Transferencia\n21 de febrero de 2026 - 09:39\n$-1.857,\n00"
|
||
// "Transferencia externa de NOELIA ISABEL LEZCANO\n21 de febrero de 2026 - 09:35\n$123,\n45"
|
||
QList<TransactionInfo> result;
|
||
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className != "android.widget.ImageView") continue;
|
||
if (!node.contentDesc.startsWith("Transferencia")) continue;
|
||
|
||
QStringList lines = node.contentDesc.split('\n');
|
||
if (lines.size() < 3) continue;
|
||
|
||
TransactionInfo tx;
|
||
|
||
// Строка 1: описание, может содержать имя отправителя
|
||
// "Transferencia" или "Transferencia externa de NOELIA ISABEL LEZCANO"
|
||
const QString descLine = lines[0].trimmed();
|
||
tx.description = descLine;
|
||
if (descLine.contains("externa de ")) {
|
||
tx.name = descLine.mid(descLine.indexOf("externa de ") + 11).trimmed();
|
||
}
|
||
|
||
// Строка 2: дата "21 de febrero de 2026 - 09:39"
|
||
tx.bankTime = lines[1].trimmed();
|
||
|
||
// Строки 3+: сумма "$-1.857,\n00" → объединяем
|
||
QString amountStr;
|
||
for (int i = 2; i < lines.size(); ++i) {
|
||
amountStr += lines[i].trimmed();
|
||
}
|
||
// "$-1.857,00" → убираем "$", "." (тысячный разделитель), заменяем "," на "."
|
||
amountStr.remove('$');
|
||
amountStr.remove(' ');
|
||
bool isNegative = amountStr.contains('-');
|
||
amountStr.remove('-');
|
||
amountStr.remove('.'); // тысячный разделитель
|
||
amountStr.replace(',', '.');
|
||
bool ok = false;
|
||
tx.amount = amountStr.toDouble(&ok);
|
||
if (ok && isNegative) tx.amount = -tx.amount;
|
||
|
||
tx.type = TransactionType::Card;
|
||
|
||
result.append(tx);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
QDateTime ScreenXmlParser::parseSpanishDate(const QString &dateStr) {
|
||
// Формат: "21 de febrero de 2026 - 09:39"
|
||
static const QMap<QString, int> spanishMonths = {
|
||
{"enero", 1}, {"febrero", 2}, {"marzo", 3}, {"abril", 4},
|
||
{"mayo", 5}, {"junio", 6}, {"julio", 7}, {"agosto", 8},
|
||
{"septiembre", 9}, {"octubre", 10}, {"noviembre", 11}, {"diciembre", 12}
|
||
};
|
||
|
||
QRegularExpression re(R"((\d{1,2})\s+de\s+(\w+)\s+de\s+(\d{4})\s*-\s*(\d{2}):(\d{2}))");
|
||
QRegularExpressionMatch match = re.match(dateStr);
|
||
if (!match.hasMatch()) return {};
|
||
|
||
const int day = match.captured(1).toInt();
|
||
const QString monthName = match.captured(2).toLower();
|
||
const int year = match.captured(3).toInt();
|
||
const int hour = match.captured(4).toInt();
|
||
const int minute = match.captured(5).toInt();
|
||
|
||
const int month = spanishMonths.value(monthName, 0);
|
||
if (month == 0) return {};
|
||
|
||
return QDateTime(QDate(year, month, day), QTime(hour, minute));
|
||
}
|
||
|
||
bool ScreenXmlParser::isPayInputCardNumberScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.hint == "Ingresar CBU, CVU o Alias") {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ScreenXmlParser::isPayTransferConfirmScreen() {
|
||
// Экран с именем получателя, Alias/CBU и кнопкой "Transferir"
|
||
// Но без "Vas a transferir" (это уже финальный экран)
|
||
bool hasTransferir = false;
|
||
bool hasVasATransferir = false;
|
||
bool hasAlias = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Transferir" && node.clickable) {
|
||
hasTransferir = true;
|
||
}
|
||
if (node.contentDesc.startsWith("Vas a transferir")) {
|
||
hasVasATransferir = true;
|
||
}
|
||
if (node.contentDesc.startsWith("Alias:")) {
|
||
hasAlias = true;
|
||
}
|
||
}
|
||
return hasTransferir && hasAlias && !hasVasATransferir;
|
||
}
|
||
|
||
bool ScreenXmlParser::isPayInputSumScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.contains(QString::fromUtf8("Cuánto vas a transferír"))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ScreenXmlParser::isPayConfirmScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.startsWith("Vas a transferir")) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
bool ScreenXmlParser::isPaySuccessScreen() {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Dinero enviado!") {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
QString ScreenXmlParser::parseRecipientNameFromConfirm() {
|
||
// "Vas a transferir \na JOEL ARCHENTI"
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc.startsWith("Vas a transferir")) {
|
||
QStringList lines = node.contentDesc.split('\n');
|
||
if (lines.size() >= 2) {
|
||
QString nameLine = lines[1].trimmed();
|
||
// Убираем "a " в начале
|
||
if (nameLine.startsWith("a ")) {
|
||
nameLine = nameLine.mid(2);
|
||
}
|
||
return nameLine.trimmed();
|
||
}
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parsePaySuccessDateTime() {
|
||
// Дата/время: "12/03/26 04:01" — содержит "/" и ":"
|
||
for (const Node &node : m_nodes) {
|
||
const QString desc = node.contentDesc.trimmed();
|
||
if (desc.contains('/') && desc.contains(':') && desc.length() <= 20) {
|
||
return desc;
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
QString ScreenXmlParser::parsePaySuccessCoelsaId() {
|
||
// "Id Coelsa" → следующий node содержит ID
|
||
bool afterIdCoelsa = false;
|
||
for (const Node &node : m_nodes) {
|
||
if (node.contentDesc == "Id Coelsa") {
|
||
afterIdCoelsa = true;
|
||
continue;
|
||
}
|
||
if (afterIdCoelsa && !node.contentDesc.isEmpty()) {
|
||
return node.contentDesc.trimmed();
|
||
}
|
||
}
|
||
return {};
|
||
}
|
||
|
||
Node ScreenXmlParser::findEditTextByHint(const QString &hint) {
|
||
for (const Node &node : m_nodes) {
|
||
if (node.className == "android.widget.EditText" && node.hint == hint) {
|
||
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 {};
|
||
}
|
||
|
||
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 {};
|
||
}
|
||
|
||
} // namespace Black
|