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") }