Implemented `CommonScript` as a base class with reusable functionality for automation workflows, including PIN input, app navigation, and server communication. Added specialized scripts like `GetCardInfoScript`, `GetProfileInfoScript`, and `LoginAndCheckAccountsScript` for card retrieval, profile parsing, and login validation. Introduced `ScreenXmlParser` for efficient XML-based UI parsing and node detection.
310 lines
9.0 KiB
C++
310 lines
9.0 KiB
C++
#include "ScreenXmlParser.h"
|
|
|
|
#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 {};
|
|
}
|
|
|
|
} // namespace Black
|