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 2403efa1..18cdc66f 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
@@ -42,6 +42,11 @@ class ScraperAccessibilityService : AccessibilityService() {
private val taxiFCollectedPrices = mutableListOf>()
private var taxiFPriceScrollAttempts = 0
+ // Multi-trip batch fields
+ private val taxiFMultiTripTrips = mutableListOf()
+ private var taxiFMultiTripIndex = 0
+ private var taxiFPriceSubmitTime = 0L
+
companion object {
private val TAXIF_EXCLUDED_TEXTS = setOf(
"JOD", "ECO", "TaxiF", "SUV", "EV", "Mini", "Female", "Van",
@@ -85,11 +90,25 @@ class ScraperAccessibilityService : AccessibilityService() {
currentTask = task
currentAppName = task.optString("app")
val taskId = task.optString("task_id")
+ val taskType = task.optString("type", "price_check")
- Log.i(TAG, "Received Task: $taskId for App: $currentAppName")
+ Log.i(TAG, "Received Task: $taskId for App: $currentAppName, Type: $taskType")
taxiFPickupDone = false
taxiFDestinationDone = false
+ taxiFMultiTripTrips.clear()
+ taxiFMultiTripIndex = 0
+ taxiFPriceSubmitTime = 0L
+
+ if (taskType == "batch_multi_trip") {
+ val tripsJson = task.optJSONArray("trips")
+ if (tripsJson != null) {
+ for (i in 0 until tripsJson.length()) {
+ taxiFMultiTripTrips.add(tripsJson.getJSONObject(i))
+ }
+ }
+ Log.i(TAG, "TaxiF: Batch multi-trip loaded with ${taxiFMultiTripTrips.size} trips")
+ }
currentState = BotState.LAUNCHING_APP
// Launch the App
@@ -365,17 +384,26 @@ class ScraperAccessibilityService : AccessibilityService() {
val task = currentTask ?: return
val taskId = task.optString("task_id")
- // Extract nested payload object where coordinates reside
- val payload = task.optJSONObject("payload")
- val startLat = payload?.optDouble("start_lat", 0.0) ?: 0.0
- val startLng = payload?.optDouble("start_lng", 0.0) ?: 0.0
- val endLat = payload?.optDouble("end_lat", 0.0) ?: 0.0
- val endLng = payload?.optDouble("end_lng", 0.0) ?: 0.0
+ val startLat: Double
+ val startLng: Double
+ val endLat: Double
+ val endLng: Double
+
+ val tripCoords = getCurrentTripLatLng()
+ if (tripCoords != null) {
+ startLat = tripCoords.first[0]
+ startLng = tripCoords.first[1]
+ endLat = tripCoords.second[0]
+ endLng = tripCoords.second[1]
+ } else {
+ val payload = task.optJSONObject("payload")
+ startLat = payload?.optDouble("start_lat", 0.0) ?: 0.0
+ startLng = payload?.optDouble("start_lng", 0.0) ?: 0.0
+ endLat = payload?.optDouble("end_lat", 0.0) ?: 0.0
+ endLng = payload?.optDouble("end_lng", 0.0) ?: 0.0
+ }
- // Calculate distance
val distanceKm = calculateDistanceInKm(startLat, startLng, endLat, endLng)
-
- // Extract numeric digits from price
val numericPrice = rawPrice.replace(Regex("[^0-9.]"), "").toDoubleOrNull() ?: 0.0
serviceScope.launch {
@@ -396,10 +424,6 @@ class ScraperAccessibilityService : AccessibilityService() {
} else {
Log.e(TAG, "Failed to submit price.")
}
-
- // Go back to IDLE
- currentState = BotState.IDLE
- currentTask = null
}
}
@@ -486,7 +510,10 @@ class ScraperAccessibilityService : AccessibilityService() {
private fun handleTaxiFAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) {
val task = currentTask ?: return
- Log.d(TAG, "TaxiF Automation event. State: $currentState")
+ // 3-second cool-down between multi-trips
+ if ((System.currentTimeMillis() - taxiFPriceSubmitTime) < 3000) return
+ Log.d(TAG, "TaxiF Automation event. State: $currentState" +
+ if (taxiFMultiTripTrips.isNotEmpty()) " Trip ${taxiFMultiTripIndex + 1}/${taxiFMultiTripTrips.size}" else "")
when (currentState) {
BotState.NAVIGATING_HOME -> {
if (hasJustClickedHome()) return
@@ -512,7 +539,7 @@ class ScraperAccessibilityService : AccessibilityService() {
BotState.SEARCHING_START -> {
if (detectTaxiFPriceScreen(rootNode)) {
if (hasJustClickedLocation() || hasJustClickedSuggestion()) return
- if (!taxiFPickupDone && shouldEditLocation(rootNode, task, isPickup = true)) {
+ if (!taxiFPickupDone && shouldEditLocation(rootNode, isPickup = true)) {
clickLocationRow(rootNode, isPickup = true)
taxiFLocationClickTime = System.currentTimeMillis()
return
@@ -521,7 +548,7 @@ class ScraperAccessibilityService : AccessibilityService() {
currentState = BotState.SEARCHING_END
return
}
- if (!taxiFPickupDone && handleTaxiFSearchScreen(rootNode, task.optString("start_location", "Amman"))) {
+ if (!taxiFPickupDone && handleTaxiFSearchScreen(rootNode, getCurrentStartLocation())) {
taxiFPickupDone = true
Log.i(TAG, "TaxiF: Handled pickup search, waiting for price screen")
}
@@ -529,7 +556,7 @@ class ScraperAccessibilityService : AccessibilityService() {
BotState.SEARCHING_END -> {
if (detectTaxiFPriceScreen(rootNode)) {
if (hasJustClickedLocation() || hasJustClickedSuggestion()) return
- if (!taxiFDestinationDone && shouldEditLocation(rootNode, task, isPickup = false)) {
+ if (!taxiFDestinationDone && shouldEditLocation(rootNode, isPickup = false)) {
clickLocationRow(rootNode, isPickup = false)
taxiFLocationClickTime = System.currentTimeMillis()
return
@@ -540,7 +567,7 @@ class ScraperAccessibilityService : AccessibilityService() {
taxiFPriceScrollAttempts = 0
return
}
- if (!taxiFDestinationDone && handleTaxiFSearchScreen(rootNode, task.optString("end_location", "Airport"))) {
+ if (!taxiFDestinationDone && handleTaxiFSearchScreen(rootNode, getCurrentEndLocation())) {
taxiFDestinationDone = true
Log.i(TAG, "TaxiF: Handled dest search, waiting for price screen")
}
@@ -566,6 +593,30 @@ class ScraperAccessibilityService : AccessibilityService() {
return (System.currentTimeMillis() - taxiFSuggestionClickTime) < 1500
}
+ private fun getCurrentStartLocation(): String {
+ return if (taxiFMultiTripTrips.isNotEmpty() && taxiFMultiTripIndex < taxiFMultiTripTrips.size)
+ taxiFMultiTripTrips[taxiFMultiTripIndex].optString("start_location", "Amman")
+ else
+ currentTask?.optString("start_location", "Amman") ?: "Amman"
+ }
+
+ private fun getCurrentEndLocation(): String {
+ return if (taxiFMultiTripTrips.isNotEmpty() && taxiFMultiTripIndex < taxiFMultiTripTrips.size)
+ taxiFMultiTripTrips[taxiFMultiTripIndex].optString("end_location", "Airport")
+ else
+ currentTask?.optString("end_location", "Airport") ?: "Airport"
+ }
+
+ private fun getCurrentTripLatLng(): Pair? {
+ return if (taxiFMultiTripTrips.isNotEmpty() && taxiFMultiTripIndex < taxiFMultiTripTrips.size) {
+ val trip = taxiFMultiTripTrips[taxiFMultiTripIndex]
+ Pair(
+ doubleArrayOf(trip.optDouble("start_lat", 0.0), trip.optDouble("start_lng", 0.0)),
+ doubleArrayOf(trip.optDouble("end_lat", 0.0), trip.optDouble("end_lng", 0.0))
+ )
+ } else null
+ }
+
private fun getLocationTexts(node: android.view.accessibility.AccessibilityNodeInfo?): List {
val texts = mutableListOf()
collectLocationTexts(node, texts)
@@ -585,9 +636,9 @@ class ScraperAccessibilityService : AccessibilityService() {
}
}
- private fun shouldEditLocation(rootNode: android.view.accessibility.AccessibilityNodeInfo, task: JSONObject, isPickup: Boolean): Boolean {
+ private fun shouldEditLocation(rootNode: android.view.accessibility.AccessibilityNodeInfo, isPickup: Boolean): Boolean {
val locationTexts = getLocationTexts(rootNode)
- val taskLoc = if (isPickup) task.optString("start_location", "") else task.optString("end_location", "")
+ val taskLoc = if (isPickup) getCurrentStartLocation() else getCurrentEndLocation()
if (taskLoc.isEmpty()) return false
val matchFound = locationTexts.any { text ->
text.contains(taskLoc, ignoreCase = true) || taskLoc.contains(text, ignoreCase = true)
@@ -725,13 +776,40 @@ class ScraperAccessibilityService : AccessibilityService() {
Log.i(TAG, "TaxiF: Found price option: ${label.take(50)} = $price JOD")
}
val cheapest = prices.minByOrNull { it.first } ?: prices.first()
- Log.i(TAG, "TaxiF: Submitting cheapest price: ${cheapest.second.take(50)} = ${cheapest.first} JOD")
+ val tripLabel = if (taxiFMultiTripTrips.isNotEmpty())
+ "Trip ${taxiFMultiTripIndex + 1}/${taxiFMultiTripTrips.size}: " else ""
+ Log.i(TAG, "TaxiF: ${tripLabel}Cheapest price: ${cheapest.first} JOD")
submitPriceToServer("${cheapest.first} JOD")
+ advanceToNextTaxiFTrip()
} else {
- Log.w(TAG, "TaxiF: No JOD prices found, falling back to generic search")
- searchPriceByCurrency(rootNode)
+ Log.w(TAG, "TaxiF: No JOD prices found${if (taxiFMultiTripTrips.isNotEmpty()) " for trip ${taxiFMultiTripIndex + 1}" else ""}")
+ if (taxiFMultiTripTrips.isEmpty()) {
+ searchPriceByCurrency(rootNode)
+ currentState = BotState.IDLE
+ }
+ }
+ }
+
+ private fun advanceToNextTaxiFTrip() {
+ taxiFPriceSubmitTime = System.currentTimeMillis()
+ if (taxiFMultiTripTrips.isNotEmpty()) {
+ taxiFMultiTripIndex++
+ taxiFPickupDone = false
+ taxiFDestinationDone = false
+
+ if (taxiFMultiTripIndex < taxiFMultiTripTrips.size) {
+ Log.i(TAG, "TaxiF: 3s cool-down then trip ${taxiFMultiTripIndex + 1}/${taxiFMultiTripTrips.size}")
+ currentState = BotState.SEARCHING_START
+ } else {
+ Log.i(TAG, "TaxiF: All ${taxiFMultiTripTrips.size} trips completed!")
+ currentState = BotState.IDLE
+ currentTask = null
+ taxiFMultiTripTrips.clear()
+ taxiFMultiTripIndex = 0
+ }
+ } else {
+ currentState = BotState.IDLE
}
- currentState = BotState.IDLE
}
private fun findHorizontalRecyclerView(node: android.view.accessibility.AccessibilityNodeInfo?): android.view.accessibility.AccessibilityNodeInfo? {
diff --git a/backend/bot/standalone_worker.php b/backend/bot/standalone_worker.php
index 48ae4832..50a2227d 100644
--- a/backend/bot/standalone_worker.php
+++ b/backend/bot/standalone_worker.php
@@ -164,6 +164,67 @@ if (isset($_POST['action'])) {
$message = "Please fill in all required fields.";
$msgType = 'error';
}
+ } elseif ($_POST['action'] === 'generate_10_trips') {
+ $app = $_POST['app'] ?? 'com.taxif.passenger';
+
+ $ammanLocations = [
+ ['name' => 'Abdoun', 'lat' => 31.9392, 'lng' => 35.8942],
+ ['name' => 'Jabal Amman', 'lat' => 31.9511, 'lng' => 35.9189],
+ ['name' => 'Sweileh', 'lat' => 32.0167, 'lng' => 35.8333],
+ ['name' => 'Khalda', 'lat' => 31.9861, 'lng' => 35.8450],
+ ['name' => 'Al-Jubaiha', 'lat' => 32.0194, 'lng' => 35.8753],
+ ['name' => 'Tla Al-Ali', 'lat' => 31.9961, 'lng' => 35.8647],
+ ['name' => 'Shmeisani', 'lat' => 31.9680, 'lng' => 35.9020],
+ ['name' => 'Um Uthaina', 'lat' => 31.9610, 'lng' => 35.8770],
+ ['name' => 'Marj Al-Hamam', 'lat' => 31.9000, 'lng' => 35.8500],
+ ['name' => 'Al-Muqabalain', 'lat' => 31.8720, 'lng' => 35.8900],
+ ['name' => 'Al-Qweismeh', 'lat' => 31.8900, 'lng' => 35.9200],
+ ['name' => 'Hashmi Al-Janoubi', 'lat' => 31.9350, 'lng' => 35.9350],
+ ['name' => 'Al-Madina', 'lat' => 31.8500, 'lng' => 35.8000],
+ ['name' => 'Sports City', 'lat' => 31.9820, 'lng' => 35.8880],
+ ];
+
+ // 10 trip pairs with varied distances (~2km to ~17km)
+ $tripPairs = [
+ [13, 5], // Sports City → Tla Al-Ali (~2km)
+ [6, 0], // Shmeisani → Abdoun (~3km)
+ [7, 1], // Um Uthaina → Jabal Amman (~4km)
+ [3, 13], // Khalda → Sports City (~5km)
+ [4, 2], // Al-Jubaiha → Sweileh (~5km)
+ [0, 8], // Abdoun → Marj Al-Hamam (~6km)
+ [1, 10], // Jabal Amman → Al-Qweismeh (~9km)
+ [6, 9], // Shmeisani → Al-Muqabalain (~11km)
+ [2, 12], // Sweileh → Al-Madina (~17km)
+ [5, 11], // Tla Al-Ali → Hashmi (~5km)
+ ];
+
+ $trips = [];
+ foreach ($tripPairs as $i => $pair) {
+ $start = $ammanLocations[$pair[0]];
+ $end = $ammanLocations[$pair[1]];
+ $trips[] = [
+ 'trip_index' => $i,
+ 'start_location' => $start['name'],
+ 'end_location' => $end['name'],
+ 'start_lat' => $start['lat'],
+ 'start_lng' => $start['lng'],
+ 'end_lat' => $end['lat'],
+ 'end_lng' => $end['lng'],
+ ];
+ }
+
+ $taskId = "batch_" . uniqid();
+ $batchTask = [
+ 'task_id' => $taskId,
+ 'type' => 'batch_multi_trip',
+ 'app' => $app,
+ 'trips' => $trips,
+ ];
+
+ $tasks = json_decode(file_get_contents(TASKS_FILE), true);
+ $tasks[] = $batchTask;
+ file_put_contents(TASKS_FILE, json_encode($tasks, JSON_PRETTY_PRINT));
+ $message = "Batch task with 10 trips generated! Task ID: $taskId";
} elseif ($_POST['action'] === 'clear_tasks') {
file_put_contents(TASKS_FILE, json_encode([]));
$message = "Task queue cleared successfully.";
@@ -646,6 +707,20 @@ $scrapedResults = json_decode(file_get_contents(RESULTS_FILE), true);
+
+
+
@@ -697,7 +772,16 @@ $scrapedResults = json_decode(file_get_contents(RESULTS_FILE), true);
- Route: →
+ Batch: ' . $tripCount . ' trips | First: ' . htmlspecialchars($firstTrip['start_location']) . ' → ' . htmlspecialchars($firstTrip['end_location']) . '
';
+ } else {
+ echo 'Route: ' . htmlspecialchars($task['start_location']) . ' → ' . htmlspecialchars($task['end_location']) . '
';
+ }
+ ?>
Pending