Update: 2026-06-30 00:22:46

This commit is contained in:
Hamza-Ayed
2026-06-30 00:22:47 +03:00
parent 57080187e7
commit 1b285fbaea
9 changed files with 123 additions and 17 deletions

View File

@@ -5,6 +5,16 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<package android:name="ae.com.yalla.go.dubai.client" />
<package android:name="com.zakinn.app" />
<package android:name="com.bis.taxi" />
<package android:name="com.careem.acma" />
<package android:name="com.ubercab" />
<package android:name="com.taxif.passenger" />
<package android:name="me.com.easytaxi" />
</queries>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"

View File

@@ -26,7 +26,7 @@ class WorkerClient(private val context: Context) {
}
// Change this to your actual server domain
private val BASE_URL = "https://api.intaleq.xyz/bot_android/standalone_worker.php"
private val BASE_URL = "https://jordan-siro.intaleqapp.com/backend/bot/standalone_worker.php"
// For local testing use: "http://10.0.2.2:8000/standalone_worker.php"
private fun generateSignature(deviceId: String, ts: Long): String {

View File

@@ -94,6 +94,14 @@ class ScraperAccessibilityService : AccessibilityService() {
val packageName = event.packageName?.toString() ?: return
val rootNode = rootInActiveWindow ?: return
// Log layout hierarchy when a Jordanian app is open to inspect UI structure
if (packageName == "com.careem.acma" || packageName == "com.ubercab" ||
packageName == "com.taxif.passenger" || packageName == "me.com.easytaxi") {
Log.d("HierarchyDump", "--- START SCREEN HIERARCHY DUMP FOR $packageName ---")
dumpNodeHierarchy(rootNode, 0)
Log.d("HierarchyDump", "--- END SCREEN HIERARCHY DUMP FOR $packageName ---")
}
when (packageName) {
"ae.com.yalla.go.dubai.client" -> handleYallaGoAutomation(rootNode)
"com.zakinn.app" -> handleZakinnAutomation(rootNode)
@@ -497,29 +505,92 @@ class ScraperAccessibilityService : AccessibilityService() {
private fun handleJeenyAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) {
val task = currentTask ?: return
Log.d(TAG, "Jeeny Automation event. State: $currentState")
when (currentState) {
BotState.SEARCHING_START -> {
val pickupEdit = findEditableNode(rootNode)
if (pickupEdit != null) {
val startLoc = task.optString("start_location", "Amman")
val arguments = android.os.Bundle().apply {
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, startLoc)
// Look for the "Where to" button/view on the main screen
val whereToNode = findNodeByText(rootNode, "إلى اين تريد الذهاب")
?: findNodeByText(rootNode, "Where to")
?: findNodeByText(rootNode, "إلى أين")
if (whereToNode != null) {
// Click the parent clickable view if the TextView itself is not clickable
var clickableParent = whereToNode
while (clickableParent != null && !clickableParent.isClickable) {
clickableParent = clickableParent.parent
}
if (clickableParent != null) {
clickableParent.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
Log.i(TAG, "Jeeny: Clicked where to button/view.")
currentState = BotState.SEARCHING_END
} else {
whereToNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
Log.i(TAG, "Jeeny: Clicked where to button/view (direct).")
currentState = BotState.SEARCHING_END
}
} else {
// If we are already on the search screen (we don't see "Where to" main button, but we see EditTexts)
val editTexts = findAllEditTexts(rootNode)
if (editTexts.isNotEmpty()) {
Log.i(TAG, "Jeeny: Already on search screen with ${editTexts.size} input fields.")
currentState = BotState.SEARCHING_END
}
pickupEdit.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
Log.i(TAG, "Jeeny: Entered start: $startLoc")
currentState = BotState.SEARCHING_END
}
}
BotState.SEARCHING_END -> {
val destEdit = findEditableNode(rootNode)
if (destEdit != null) {
val endLoc = task.optString("end_location", "Airport")
val arguments = android.os.Bundle().apply {
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc)
val endLoc = task.optString("end_location", "Airport")
val firstWord = endLoc.split(" ").firstOrNull() ?: ""
val resultNode = if (firstWord.isNotEmpty()) findNodeByText(rootNode, firstWord) else null
val isEditText = resultNode?.className == "android.widget.EditText" || resultNode?.isEditable == true
// If a search result item containing the target location words is found (not the input field itself)
if (resultNode != null && !isEditText) {
var clickableParent = resultNode
while (clickableParent != null && !clickableParent.isClickable) {
clickableParent = clickableParent.parent
}
if (clickableParent != null) {
clickableParent.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
Log.i(TAG, "Jeeny: Clicked search result containing '$firstWord'.")
currentState = BotState.READING_PRICE
return
}
}
// Otherwise, enter the locations to populate the list
val editTexts = findAllEditTexts(rootNode)
if (editTexts.isNotEmpty()) {
val startLoc = task.optString("start_location", "Amman")
if (editTexts.size >= 2) {
val pickupField = editTexts[0]
val destField = editTexts[1]
if (pickupField.text?.toString() != startLoc) {
val pickupArgs = android.os.Bundle().apply {
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, startLoc)
}
pickupField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, pickupArgs)
Log.i(TAG, "Jeeny: Set pickup -> $startLoc")
}
if (destField.text?.toString() != endLoc) {
val destArgs = android.os.Bundle().apply {
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc)
}
destField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, destArgs)
Log.i(TAG, "Jeeny: Set destination -> $endLoc")
}
} else {
val destField = editTexts[0]
if (destField.text?.toString() != endLoc) {
val destArgs = android.os.Bundle().apply {
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc)
}
destField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, destArgs)
Log.i(TAG, "Jeeny: Set single field -> $endLoc")
}
}
destEdit.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
Log.i(TAG, "Jeeny: Entered end: $endLoc")
currentState = BotState.READING_PRICE
}
}
BotState.READING_PRICE -> {
@@ -529,6 +600,31 @@ class ScraperAccessibilityService : AccessibilityService() {
}
}
private fun dumpNodeHierarchy(node: android.view.accessibility.AccessibilityNodeInfo?, depth: Int) {
if (node == null) return
val indent = " ".repeat(depth)
val className = node.className ?: "unknown"
val text = node.text ?: ""
val resourceId = node.viewIdResourceName ?: "no-id"
val isClickable = node.isClickable
val isEditable = node.isEditable
Log.d("HierarchyDump", "$indent[$className] ID: $resourceId | Text: \"$text\" | Clickable: $isClickable | Editable: $isEditable")
for (i in 0 until node.childCount) {
dumpNodeHierarchy(node.getChild(i), depth + 1)
}
}
private fun findAllEditTexts(node: android.view.accessibility.AccessibilityNodeInfo?, list: MutableList<android.view.accessibility.AccessibilityNodeInfo> = mutableListOf()): List<android.view.accessibility.AccessibilityNodeInfo> {
if (node == null) return list
if (node.className == "android.widget.EditText" || node.isEditable) {
list.add(node)
}
for (i in 0 until node.childCount) {
findAllEditTexts(node.getChild(i), list)
}
return list
}
override fun onInterrupt() {
Log.w(TAG, "Accessibility Service Interrupted")
}