diff --git a/android_bot/.gradle/8.13/executionHistory/executionHistory.bin b/android_bot/.gradle/8.13/executionHistory/executionHistory.bin index b849b7bd..83234c17 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 2a972215..b5566cd5 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 514a5f28..aef68768 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 59bf925b..262bfb60 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 8654902e..943d8219 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 c01194a3..6099b77a 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 9cb0c785..c8642adb 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 @@ -10,6 +10,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.json.JSONObject enum class BotState { @@ -34,9 +35,11 @@ class ScraperAccessibilityService : AccessibilityService() { private var currentState = BotState.IDLE private var currentTask: JSONObject? = null private var currentAppName: String = "" + private var appLaunchTime = 0L private var taxiFHomeClickTime = 0L private var taxiFLocationClickTime = 0L private var taxiFSuggestionClickTime = 0L + private var taxiFSearchTypingTime = 0L private var taxiFPickupDone = false private var taxiFDestinationDone = false private val taxiFCollectedPrices = mutableListOf>() @@ -75,9 +78,20 @@ class ScraperAccessibilityService : AccessibilityService() { } else { Log.d(TAG, "No tasks available.") } + // Poll every 5 seconds + delay(5000) + } else { + // We have an active task. Just to be safe, process screen periodically + // in case accessibility events are missed (e.g. app launched but screen static) + withContext(Dispatchers.Main) { + val rootNode = rootInActiveWindow + if (rootNode != null) { + val pkg = rootNode.packageName?.toString() ?: currentAppName + dispatchAutomation(pkg, rootNode) + } + } + delay(1000) // Poll UI state every 1s when active } - // Poll every 5 seconds - delay(5000) } } } @@ -91,11 +105,13 @@ class ScraperAccessibilityService : AccessibilityService() { taxiFPickupDone = false taxiFDestinationDone = false + taxiFSearchTypingTime = 0L currentState = BotState.LAUNCHING_APP // Launch the App val success = AppLauncher.launchApp(this, currentAppName) if (success) { + appLaunchTime = System.currentTimeMillis() currentState = if (currentAppName == "taxif" || currentAppName == "com.taxif.passenger") BotState.NAVIGATING_HOME else BotState.SEARCHING_START Log.i(TAG, "State -> ${currentState}") } else { @@ -120,14 +136,18 @@ class ScraperAccessibilityService : AccessibilityService() { Log.d("HierarchyDump", "--- END SCREEN HIERARCHY DUMP FOR $packageName ---") } - when (packageName) { - "ae.com.yalla.go.dubai.client" -> handleYallaGoAutomation(rootNode) - "com.zakinn.app" -> handleZakinnAutomation(rootNode) - "com.bis.taxi" -> handleTfadalAutomation(rootNode) - "com.careem.acma" -> handleCareemAutomation(rootNode) - "com.ubercab" -> handleUberAutomation(rootNode) - "com.taxif.passenger" -> handleTaxiFAutomation(rootNode) - "me.com.easytaxi" -> handleJeenyAutomation(rootNode) + dispatchAutomation(packageName, rootNode) + } + + private fun dispatchAutomation(packageName: String, rootNode: android.view.accessibility.AccessibilityNodeInfo) { + when { + packageName.contains("yalla") -> handleYallaGoAutomation(rootNode) + packageName.contains("zakinn") -> handleZakinnAutomation(rootNode) + packageName.contains("bis.taxi") -> handleTfadalAutomation(rootNode) + packageName.contains("careem") -> handleCareemAutomation(rootNode) + packageName.contains("ubercab") -> handleUberAutomation(rootNode) + packageName.contains("taxif") -> handleTaxiFAutomation(rootNode) + packageName.contains("easytaxi") -> handleJeenyAutomation(rootNode) } } @@ -405,7 +425,9 @@ class ScraperAccessibilityService : AccessibilityService() { if (nextResult != null && nextResult.optBoolean("has_task", false)) { val nextTask = nextResult.getJSONObject("task") currentState = BotState.IDLE - handleTask(nextTask) + withContext(Dispatchers.Main) { + handleTask(nextTask) + } Log.i(TAG, "Multi-trip: Immediately started next task ${nextTask.optString("task_id")}") } else { Log.i(TAG, "No more tasks, returning to IDLE.") @@ -501,8 +523,9 @@ class ScraperAccessibilityService : AccessibilityService() { Log.d(TAG, "TaxiF Automation event. State: $currentState") when (currentState) { BotState.NAVIGATING_HOME -> { + if (System.currentTimeMillis() - appLaunchTime < 2000) return if (hasJustClickedHome()) return - val rihlaNode = findNodeByText(rootNode, "رحلة") + val rihlaNode = findNodeByText(rootNode, "رحلة") ?: findNodeByText(rootNode, "طلب") if (rihlaNode != null) { var clickable: android.view.accessibility.AccessibilityNodeInfo? = rihlaNode while (clickable != null && !clickable.isClickable) { @@ -510,12 +533,13 @@ class ScraperAccessibilityService : AccessibilityService() { } if (clickable != null) { clickable.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) - Log.i(TAG, "TaxiF: Clicked 'رحلة' tab, entering trip flow") + Log.i(TAG, "TaxiF: Clicked 'رحلة' or 'طلب' tab, entering trip flow") } else { rihlaNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) - Log.i(TAG, "TaxiF: Clicked 'رحلة' node directly") + Log.i(TAG, "TaxiF: Clicked 'رحلة' or 'طلب' node directly") } taxiFHomeClickTime = System.currentTimeMillis() + currentState = BotState.SEARCHING_START return } currentState = BotState.SEARCHING_START @@ -661,17 +685,65 @@ class ScraperAccessibilityService : AccessibilityService() { } editField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments) Log.i(TAG, "TaxiF: Typed '$location' in search field") + taxiFSearchTypingTime = System.currentTimeMillis() + } else { + if (taxiFSearchTypingTime == 0L) { + taxiFSearchTypingTime = System.currentTimeMillis() + } } + if (hasJustClickedSuggestion()) return false + val firstWord = location.split(" ").firstOrNull() ?: location if (clickFirstSuggestion(rootNode, firstWord)) { taxiFSuggestionClickTime = System.currentTimeMillis() Log.i(TAG, "TaxiF: Clicked suggestion matching '$firstWord'") + taxiFSearchTypingTime = 0L return true + } else { + if (System.currentTimeMillis() - taxiFSearchTypingTime > 3000) { + if (clickFirstGenericSuggestion(rootNode)) { + taxiFSuggestionClickTime = System.currentTimeMillis() + Log.i(TAG, "TaxiF: Clicked first generic suggestion as fallback") + taxiFSearchTypingTime = 0L + return true + } + } } return false } + private fun clickFirstGenericSuggestion(rootNode: android.view.accessibility.AccessibilityNodeInfo?): Boolean { + if (rootNode == null) return false + val recycler = findRecyclerView(rootNode) ?: return false + for (i in 0 until recycler.childCount) { + val child = recycler.getChild(i) ?: continue + if (child.isClickable) { + child.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) + return true + } + } + for (i in 0 until recycler.childCount) { + val child = recycler.getChild(i) ?: continue + val clickableDescendant = findClickableDescendant(child) + if (clickableDescendant != null) { + clickableDescendant.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) + return true + } + } + return false + } + + private fun findClickableDescendant(node: android.view.accessibility.AccessibilityNodeInfo?): android.view.accessibility.AccessibilityNodeInfo? { + if (node == null) return null + if (node.isClickable) return node + for (i in 0 until node.childCount) { + val res = findClickableDescendant(node.getChild(i)) + if (res != null) return res + } + return null + } + private fun clickFirstSuggestion(rootNode: android.view.accessibility.AccessibilityNodeInfo?, target: String): Boolean { if (rootNode == null) return false val recycler = findRecyclerView(rootNode) ?: return false