diff --git a/android_bot/.gradle/8.13/executionHistory/executionHistory.bin b/android_bot/.gradle/8.13/executionHistory/executionHistory.bin index 83234c17..be308351 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 b5566cd5..695415d2 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 aef68768..e6c4a2de 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 262bfb60..913ffc03 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 943d8219..5b3e1968 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 6099b77a..eb33a3c7 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/java/com/siro/android_bot/service/ScraperAccessibilityService.kt b/android_bot/app/src/main/java/com/siro/android_bot/service/ScraperAccessibilityService.kt index c8642adb..2175a2f6 100644 --- a/android_bot/app/src/main/java/com/siro/android_bot/service/ScraperAccessibilityService.kt +++ b/android_bot/app/src/main/java/com/siro/android_bot/service/ScraperAccessibilityService.kt @@ -46,6 +46,11 @@ class ScraperAccessibilityService : AccessibilityService() { private var taxiFPriceScrollAttempts = 0 private var taxiFReadingPriceStartTime = 0L + // Jeeny State Memory + private var jeenyDestinationDone = false + private var jeenyPickupDone = false + private var jeenySearchTypingTime = 0L + private var jeenyPickupWaitStartTime = 0L companion object { private val TAXIF_EXCLUDED_TEXTS = setOf( "JOD", "ECO", "TaxiF", "SUV", "EV", "Mini", "Female", "Van", @@ -872,6 +877,28 @@ class ScraperAccessibilityService : AccessibilityService() { return null } + private fun getJeenyEcoLitePrice(rootNode: android.view.accessibility.AccessibilityNodeInfo?): Double? { + if (rootNode == null) return null + val ecoNode = findNodeByText(rootNode, "Eco lite") + ?: findNodeByText(rootNode, "EcoLite") + ?: findNodeByText(rootNode, "ايكو لايت") + ?: findNodeByText(rootNode, "إيكو لايت") + + if (ecoNode != null) { + var parent = ecoNode.parent + for (i in 0..2) { // search up to 3 levels up + if (parent == null) break + val prices = mutableListOf>() + findAllJODPrices(parent, prices) + if (prices.isNotEmpty()) { + return prices.first().first + } + parent = parent.parent + } + } + return null + } + private fun handleJeenyAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) { val task = currentTask ?: return Log.d(TAG, "Jeeny Automation event. State: $currentState") @@ -879,7 +906,8 @@ class ScraperAccessibilityService : AccessibilityService() { when (currentState) { BotState.SEARCHING_START -> { // Look for the "Where to" button/view on the main screen - val whereToNode = findNodeByText(rootNode, "إلى اين تريد الذهاب") + val whereToNode = findNodeByText(rootNode, "موقع الوصول") + ?: findNodeByText(rootNode, "إلى اين تريد الذهاب") ?: findNodeByText(rootNode, "Where to") ?: findNodeByText(rootNode, "إلى أين") @@ -892,10 +920,18 @@ class ScraperAccessibilityService : AccessibilityService() { if (clickableParent != null) { clickableParent.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) Log.i(TAG, "Jeeny: Clicked where to button/view.") + jeenyDestinationDone = false + jeenyPickupDone = false + jeenySearchTypingTime = 0L + jeenyPickupWaitStartTime = 0L currentState = BotState.SEARCHING_END } else { whereToNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) Log.i(TAG, "Jeeny: Clicked where to button/view (direct).") + jeenyDestinationDone = false + jeenyPickupDone = false + jeenySearchTypingTime = 0L + jeenyPickupWaitStartTime = 0L currentState = BotState.SEARCHING_END } } else { @@ -908,57 +944,77 @@ class ScraperAccessibilityService : AccessibilityService() { } } BotState.SEARCHING_END -> { - 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 (!jeenyDestinationDone) { + val endLoc = task.optString("end_location", "Airport") - 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 editTexts = findAllEditTexts(rootNode) + if (editTexts.isNotEmpty()) { + val destField = editTexts.last() // Destination is usually the last one + if (destField.text?.toString() != endLoc && !destField.text.toString().contains(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") + jeenySearchTypingTime = System.currentTimeMillis() + return } - } 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) + } + + if (jeenySearchTypingTime > 0 && System.currentTimeMillis() - jeenySearchTypingTime > 1500) { + val firstWord = endLoc.split(" ").firstOrNull() ?: endLoc + if (clickFirstSuggestion(rootNode, firstWord) || clickFirstGenericSuggestion(rootNode)) { + Log.i(TAG, "Jeeny: Clicked destination suggestion") + jeenyDestinationDone = true + jeenyPickupWaitStartTime = System.currentTimeMillis() + jeenySearchTypingTime = 0L + return + } + } + } else if (!jeenyPickupDone) { + // Wait 2 seconds for pickup widget to appear + if (jeenyPickupWaitStartTime > 0 && System.currentTimeMillis() - jeenyPickupWaitStartTime < 2000) { + return + } + + val startLoc = task.optString("start_location", "Amman") + + // The pickup search box might just be an EditText or we might need to click "موقع الانطلاق" first + val pickupTextNode = findNodeByText(rootNode, "موقع الانطلاق") + if (pickupTextNode != null) { + var clickableParent = pickupTextNode + while (clickableParent != null && !clickableParent.isClickable) { + clickableParent = clickableParent.parent + } + if (clickableParent != null) { + clickableParent.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) + Log.i(TAG, "Jeeny: Clicked pickup location widget") + jeenyPickupWaitStartTime = 0L // prevent re-clicking immediately + return + } + } + + val editTexts = findAllEditTexts(rootNode) + if (editTexts.isNotEmpty()) { + val pickupField = editTexts.first() // Pickup is usually the first one when both are visible + if (pickupField.text?.toString() != startLoc && !pickupField.text.toString().contains(startLoc)) { + val pickupArgs = android.os.Bundle().apply { + putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, startLoc) } - destField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, destArgs) - Log.i(TAG, "Jeeny: Set single field -> $endLoc") + pickupField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, pickupArgs) + Log.i(TAG, "Jeeny: Set pickup -> $startLoc") + jeenySearchTypingTime = System.currentTimeMillis() + return + } + } + + if (jeenySearchTypingTime > 0 && System.currentTimeMillis() - jeenySearchTypingTime > 1500) { + val firstWord = startLoc.split(" ").firstOrNull() ?: startLoc + if (clickFirstSuggestion(rootNode, firstWord) || clickFirstGenericSuggestion(rootNode)) { + Log.i(TAG, "Jeeny: Clicked pickup suggestion") + jeenyPickupDone = true + currentState = BotState.READING_PRICE + return } } } @@ -983,7 +1039,15 @@ class ScraperAccessibilityService : AccessibilityService() { } // If not on pickup confirmation screen, read the price - searchPriceByCurrency(rootNode) + val ecoLitePrice = getJeenyEcoLitePrice(rootNode) + if (ecoLitePrice != null && ecoLitePrice > 0) { + Log.i(TAG, "Jeeny: Found Eco lite price: $ecoLitePrice JOD") + submitPriceToServer("$ecoLitePrice JOD") + currentState = BotState.IDLE + } else { + // Fallback to searching any price if eco lite isn't strictly matched + searchPriceByCurrency(rootNode) + } } else -> {} }