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); + + +
+ + +

Batch Multi-Trip Generator

+

+ Generates 1 batch task containing 10 varied Amman trips (2–17 km) for TaxiF. + The bot processes all 10 trips without reopening the app. +

+ +
@@ -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