Update: 2026-06-30 14:19:01

This commit is contained in:
Hamza-Ayed
2026-06-30 14:19:01 +03:00
parent de1f5ced47
commit ef0ee91fd9
7 changed files with 109 additions and 45 deletions

View File

@@ -46,6 +46,11 @@ class ScraperAccessibilityService : AccessibilityService() {
private var taxiFPriceScrollAttempts = 0 private var taxiFPriceScrollAttempts = 0
private var taxiFReadingPriceStartTime = 0L 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 { companion object {
private val TAXIF_EXCLUDED_TEXTS = setOf( private val TAXIF_EXCLUDED_TEXTS = setOf(
"JOD", "ECO", "TaxiF", "SUV", "EV", "Mini", "Female", "Van", "JOD", "ECO", "TaxiF", "SUV", "EV", "Mini", "Female", "Van",
@@ -872,6 +877,28 @@ class ScraperAccessibilityService : AccessibilityService() {
return null 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<Pair<Double, String>>()
findAllJODPrices(parent, prices)
if (prices.isNotEmpty()) {
return prices.first().first
}
parent = parent.parent
}
}
return null
}
private fun handleJeenyAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) { private fun handleJeenyAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) {
val task = currentTask ?: return val task = currentTask ?: return
Log.d(TAG, "Jeeny Automation event. State: $currentState") Log.d(TAG, "Jeeny Automation event. State: $currentState")
@@ -879,7 +906,8 @@ class ScraperAccessibilityService : AccessibilityService() {
when (currentState) { when (currentState) {
BotState.SEARCHING_START -> { BotState.SEARCHING_START -> {
// Look for the "Where to" button/view on the main screen // 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, "Where to")
?: findNodeByText(rootNode, "إلى أين") ?: findNodeByText(rootNode, "إلى أين")
@@ -892,10 +920,18 @@ class ScraperAccessibilityService : AccessibilityService() {
if (clickableParent != null) { if (clickableParent != null) {
clickableParent.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) clickableParent.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
Log.i(TAG, "Jeeny: Clicked where to button/view.") Log.i(TAG, "Jeeny: Clicked where to button/view.")
jeenyDestinationDone = false
jeenyPickupDone = false
jeenySearchTypingTime = 0L
jeenyPickupWaitStartTime = 0L
currentState = BotState.SEARCHING_END currentState = BotState.SEARCHING_END
} else { } else {
whereToNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK) whereToNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
Log.i(TAG, "Jeeny: Clicked where to button/view (direct).") Log.i(TAG, "Jeeny: Clicked where to button/view (direct).")
jeenyDestinationDone = false
jeenyPickupDone = false
jeenySearchTypingTime = 0L
jeenyPickupWaitStartTime = 0L
currentState = BotState.SEARCHING_END currentState = BotState.SEARCHING_END
} }
} else { } else {
@@ -908,57 +944,77 @@ class ScraperAccessibilityService : AccessibilityService() {
} }
} }
BotState.SEARCHING_END -> { BotState.SEARCHING_END -> {
val endLoc = task.optString("end_location", "Airport") if (!jeenyDestinationDone) {
val firstWord = endLoc.split(" ").firstOrNull() ?: "" val endLoc = task.optString("end_location", "Airport")
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) val editTexts = findAllEditTexts(rootNode)
if (resultNode != null && !isEditText) { if (editTexts.isNotEmpty()) {
var clickableParent = resultNode val destField = editTexts.last() // Destination is usually the last one
while (clickableParent != null && !clickableParent.isClickable) { if (destField.text?.toString() != endLoc && !destField.text.toString().contains(endLoc)) {
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 { val destArgs = android.os.Bundle().apply {
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc) putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc)
} }
destField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, destArgs) destField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, destArgs)
Log.i(TAG, "Jeeny: Set destination -> $endLoc") Log.i(TAG, "Jeeny: Set destination -> $endLoc")
jeenySearchTypingTime = System.currentTimeMillis()
return
} }
} else { }
val destField = editTexts[0]
if (destField.text?.toString() != endLoc) { if (jeenySearchTypingTime > 0 && System.currentTimeMillis() - jeenySearchTypingTime > 1500) {
val destArgs = android.os.Bundle().apply { val firstWord = endLoc.split(" ").firstOrNull() ?: endLoc
putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, 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) pickupField.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, pickupArgs)
Log.i(TAG, "Jeeny: Set single field -> $endLoc") 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 // 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 -> {} else -> {}
} }