diff --git a/android_bot/.gradle/8.13/executionHistory/executionHistory.bin b/android_bot/.gradle/8.13/executionHistory/executionHistory.bin
index 5c5a01e3..965cd613 100644
Binary files a/android_bot/.gradle/8.13/executionHistory/executionHistory.bin and b/android_bot/.gradle/8.13/executionHistory/executionHistory.bin differ
diff --git a/android_bot/.gradle/8.13/executionHistory/executionHistory.lock b/android_bot/.gradle/8.13/executionHistory/executionHistory.lock
index f14641dc..eeb0ede8 100644
Binary files a/android_bot/.gradle/8.13/executionHistory/executionHistory.lock and b/android_bot/.gradle/8.13/executionHistory/executionHistory.lock differ
diff --git a/android_bot/.gradle/8.13/fileHashes/fileHashes.bin b/android_bot/.gradle/8.13/fileHashes/fileHashes.bin
index 936cbf2d..3acbfa60 100644
Binary files a/android_bot/.gradle/8.13/fileHashes/fileHashes.bin and b/android_bot/.gradle/8.13/fileHashes/fileHashes.bin differ
diff --git a/android_bot/.gradle/8.13/fileHashes/fileHashes.lock b/android_bot/.gradle/8.13/fileHashes/fileHashes.lock
index 86289929..8cb9ccae 100644
Binary files a/android_bot/.gradle/8.13/fileHashes/fileHashes.lock and b/android_bot/.gradle/8.13/fileHashes/fileHashes.lock differ
diff --git a/android_bot/.gradle/8.13/fileHashes/resourceHashesCache.bin b/android_bot/.gradle/8.13/fileHashes/resourceHashesCache.bin
index 45f9aa0b..9d8a44fd 100644
Binary files a/android_bot/.gradle/8.13/fileHashes/resourceHashesCache.bin and b/android_bot/.gradle/8.13/fileHashes/resourceHashesCache.bin differ
diff --git a/android_bot/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/android_bot/.gradle/buildOutputCleanup/buildOutputCleanup.lock
index 98ac2328..3f403c28 100644
Binary files a/android_bot/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/android_bot/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/android_bot/app/src/main/AndroidManifest.xml b/android_bot/app/src/main/AndroidManifest.xml
index e7164506..5148acb5 100644
--- a/android_bot/app/src/main/AndroidManifest.xml
+++ b/android_bot/app/src/main/AndroidManifest.xml
@@ -5,6 +5,16 @@
+
+
+
+
+
+
+
+
+
+
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 = mutableListOf()): List {
+ 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")
}