Update: 2026-06-30 04:42:17

This commit is contained in:
Hamza-Ayed
2026-06-30 04:42:18 +03:00
parent 625df23bfc
commit 8126a0ac38
7 changed files with 86 additions and 14 deletions

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONObject import org.json.JSONObject
enum class BotState { enum class BotState {
@@ -34,9 +35,11 @@ class ScraperAccessibilityService : AccessibilityService() {
private var currentState = BotState.IDLE private var currentState = BotState.IDLE
private var currentTask: JSONObject? = null private var currentTask: JSONObject? = null
private var currentAppName: String = "" private var currentAppName: String = ""
private var appLaunchTime = 0L
private var taxiFHomeClickTime = 0L private var taxiFHomeClickTime = 0L
private var taxiFLocationClickTime = 0L private var taxiFLocationClickTime = 0L
private var taxiFSuggestionClickTime = 0L private var taxiFSuggestionClickTime = 0L
private var taxiFSearchTypingTime = 0L
private var taxiFPickupDone = false private var taxiFPickupDone = false
private var taxiFDestinationDone = false private var taxiFDestinationDone = false
private val taxiFCollectedPrices = mutableListOf<Pair<Double, String>>() private val taxiFCollectedPrices = mutableListOf<Pair<Double, String>>()
@@ -75,9 +78,20 @@ class ScraperAccessibilityService : AccessibilityService() {
} else { } else {
Log.d(TAG, "No tasks available.") Log.d(TAG, "No tasks available.")
} }
}
// Poll every 5 seconds // Poll every 5 seconds
delay(5000) 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
}
} }
} }
} }
@@ -91,11 +105,13 @@ class ScraperAccessibilityService : AccessibilityService() {
taxiFPickupDone = false taxiFPickupDone = false
taxiFDestinationDone = false taxiFDestinationDone = false
taxiFSearchTypingTime = 0L
currentState = BotState.LAUNCHING_APP currentState = BotState.LAUNCHING_APP
// Launch the App // Launch the App
val success = AppLauncher.launchApp(this, currentAppName) val success = AppLauncher.launchApp(this, currentAppName)
if (success) { if (success) {
appLaunchTime = System.currentTimeMillis()
currentState = if (currentAppName == "taxif" || currentAppName == "com.taxif.passenger") BotState.NAVIGATING_HOME else BotState.SEARCHING_START currentState = if (currentAppName == "taxif" || currentAppName == "com.taxif.passenger") BotState.NAVIGATING_HOME else BotState.SEARCHING_START
Log.i(TAG, "State -> ${currentState}") Log.i(TAG, "State -> ${currentState}")
} else { } else {
@@ -120,14 +136,18 @@ class ScraperAccessibilityService : AccessibilityService() {
Log.d("HierarchyDump", "--- END SCREEN HIERARCHY DUMP FOR $packageName ---") Log.d("HierarchyDump", "--- END SCREEN HIERARCHY DUMP FOR $packageName ---")
} }
when (packageName) { dispatchAutomation(packageName, rootNode)
"ae.com.yalla.go.dubai.client" -> handleYallaGoAutomation(rootNode) }
"com.zakinn.app" -> handleZakinnAutomation(rootNode)
"com.bis.taxi" -> handleTfadalAutomation(rootNode) private fun dispatchAutomation(packageName: String, rootNode: android.view.accessibility.AccessibilityNodeInfo) {
"com.careem.acma" -> handleCareemAutomation(rootNode) when {
"com.ubercab" -> handleUberAutomation(rootNode) packageName.contains("yalla") -> handleYallaGoAutomation(rootNode)
"com.taxif.passenger" -> handleTaxiFAutomation(rootNode) packageName.contains("zakinn") -> handleZakinnAutomation(rootNode)
"me.com.easytaxi" -> handleJeenyAutomation(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)) { if (nextResult != null && nextResult.optBoolean("has_task", false)) {
val nextTask = nextResult.getJSONObject("task") val nextTask = nextResult.getJSONObject("task")
currentState = BotState.IDLE currentState = BotState.IDLE
withContext(Dispatchers.Main) {
handleTask(nextTask) handleTask(nextTask)
}
Log.i(TAG, "Multi-trip: Immediately started next task ${nextTask.optString("task_id")}") Log.i(TAG, "Multi-trip: Immediately started next task ${nextTask.optString("task_id")}")
} else { } else {
Log.i(TAG, "No more tasks, returning to IDLE.") Log.i(TAG, "No more tasks, returning to IDLE.")
@@ -501,8 +523,9 @@ class ScraperAccessibilityService : AccessibilityService() {
Log.d(TAG, "TaxiF Automation event. State: $currentState") Log.d(TAG, "TaxiF Automation event. State: $currentState")
when (currentState) { when (currentState) {
BotState.NAVIGATING_HOME -> { BotState.NAVIGATING_HOME -> {
if (System.currentTimeMillis() - appLaunchTime < 2000) return
if (hasJustClickedHome()) return if (hasJustClickedHome()) return
val rihlaNode = findNodeByText(rootNode, "رحلة") val rihlaNode = findNodeByText(rootNode, "رحلة") ?: findNodeByText(rootNode, "طلب")
if (rihlaNode != null) { if (rihlaNode != null) {
var clickable: android.view.accessibility.AccessibilityNodeInfo? = rihlaNode var clickable: android.view.accessibility.AccessibilityNodeInfo? = rihlaNode
while (clickable != null && !clickable.isClickable) { while (clickable != null && !clickable.isClickable) {
@@ -510,12 +533,13 @@ class ScraperAccessibilityService : AccessibilityService() {
} }
if (clickable != null) { if (clickable != null) {
clickable.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) 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 { } else {
rihlaNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) 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() taxiFHomeClickTime = System.currentTimeMillis()
currentState = BotState.SEARCHING_START
return return
} }
currentState = BotState.SEARCHING_START currentState = BotState.SEARCHING_START
@@ -661,17 +685,65 @@ class ScraperAccessibilityService : AccessibilityService() {
} }
editField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments) editField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
Log.i(TAG, "TaxiF: Typed '$location' in search field") Log.i(TAG, "TaxiF: Typed '$location' in search field")
taxiFSearchTypingTime = System.currentTimeMillis()
} else {
if (taxiFSearchTypingTime == 0L) {
taxiFSearchTypingTime = System.currentTimeMillis()
} }
}
if (hasJustClickedSuggestion()) return false if (hasJustClickedSuggestion()) return false
val firstWord = location.split(" ").firstOrNull() ?: location val firstWord = location.split(" ").firstOrNull() ?: location
if (clickFirstSuggestion(rootNode, firstWord)) { if (clickFirstSuggestion(rootNode, firstWord)) {
taxiFSuggestionClickTime = System.currentTimeMillis() taxiFSuggestionClickTime = System.currentTimeMillis()
Log.i(TAG, "TaxiF: Clicked suggestion matching '$firstWord'") 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 true
} }
}
}
return false 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 { private fun clickFirstSuggestion(rootNode: android.view.accessibility.AccessibilityNodeInfo?, target: String): Boolean {
if (rootNode == null) return false if (rootNode == null) return false
val recycler = findRecyclerView(rootNode) ?: return false val recycler = findRecyclerView(rootNode) ?: return false