diff --git a/.gradle_home/wrapper/dists/gradle-8.13-bin/5xuhj0ry160q40clulazy9h7d/gradle-8.13-bin.zip.lck b/.gradle_home/wrapper/dists/gradle-8.13-bin/5xuhj0ry160q40clulazy9h7d/gradle-8.13-bin.zip.lck
new file mode 100644
index 0000000..e69de29
diff --git a/.gradle_home/wrapper/dists/gradle-8.13-bin/5xuhj0ry160q40clulazy9h7d/gradle-8.13-bin.zip.part b/.gradle_home/wrapper/dists/gradle-8.13-bin/5xuhj0ry160q40clulazy9h7d/gradle-8.13-bin.zip.part
new file mode 100644
index 0000000..e69de29
diff --git a/android_bot/.gradle/8.13/checksums/checksums.lock b/android_bot/.gradle/8.13/checksums/checksums.lock
index 54a0ddf..da21be0 100644
Binary files a/android_bot/.gradle/8.13/checksums/checksums.lock and b/android_bot/.gradle/8.13/checksums/checksums.lock differ
diff --git a/android_bot/.gradle/8.13/checksums/md5-checksums.bin b/android_bot/.gradle/8.13/checksums/md5-checksums.bin
index 9d46967..4463403 100644
Binary files a/android_bot/.gradle/8.13/checksums/md5-checksums.bin and b/android_bot/.gradle/8.13/checksums/md5-checksums.bin differ
diff --git a/android_bot/.gradle/8.13/checksums/sha1-checksums.bin b/android_bot/.gradle/8.13/checksums/sha1-checksums.bin
index f261c43..874c193 100644
Binary files a/android_bot/.gradle/8.13/checksums/sha1-checksums.bin and b/android_bot/.gradle/8.13/checksums/sha1-checksums.bin differ
diff --git a/android_bot/.gradle/8.13/executionHistory/executionHistory.bin b/android_bot/.gradle/8.13/executionHistory/executionHistory.bin
new file mode 100644
index 0000000..040e87c
Binary files /dev/null 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 8f4ba1c..7d8f046 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 3ee70d3..7d8fe4a 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 f9f46f1..8d4d4a3 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 19d5076..669b055 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 97b1e96..8c1fd74 100644
Binary files a/android_bot/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/android_bot/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ
diff --git a/android_bot/.gradle/buildOutputCleanup/outputFiles.bin b/android_bot/.gradle/buildOutputCleanup/outputFiles.bin
new file mode 100644
index 0000000..2d8ab2c
Binary files /dev/null and b/android_bot/.gradle/buildOutputCleanup/outputFiles.bin differ
diff --git a/android_bot/.gradle/file-system.probe b/android_bot/.gradle/file-system.probe
new file mode 100644
index 0000000..31d1f58
Binary files /dev/null and b/android_bot/.gradle/file-system.probe differ
diff --git a/android_bot/app/src/main/AndroidManifest.xml b/android_bot/app/src/main/AndroidManifest.xml
index 3d14b48..e511a4e 100644
--- a/android_bot/app/src/main/AndroidManifest.xml
+++ b/android_bot/app/src/main/AndroidManifest.xml
@@ -2,6 +2,9 @@
+
+
+
+ android:theme="@style/Theme.Android_bot"
+ android:usesCleartextTraffic="true">
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/android_bot/app/src/main/java/com/siro/android_bot/MainActivity.kt b/android_bot/app/src/main/java/com/siro/android_bot/MainActivity.kt
index 41860b5..6a468c8 100644
--- a/android_bot/app/src/main/java/com/siro/android_bot/MainActivity.kt
+++ b/android_bot/app/src/main/java/com/siro/android_bot/MainActivity.kt
@@ -1,27 +1,46 @@
package com.siro.android_bot
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
import android.os.Bundle
+import android.provider.Settings
+import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Button
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
import com.siro.android_bot.ui.theme.Android_botTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+
+ val androidId = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN"
+
enableEdgeToEdge()
setContent {
Android_botTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
- Greeting(
- name = "Android",
+ BotSetupScreen(
+ deviceId = androidId,
modifier = Modifier.padding(innerPadding)
)
}
@@ -31,17 +50,55 @@ class MainActivity : ComponentActivity() {
}
@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Hello $name!",
- modifier = modifier
- )
+fun BotSetupScreen(deviceId: String, modifier: Modifier = Modifier) {
+ val context = LocalContext.current
+
+ Column(
+ modifier = modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Center,
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ Text(
+ text = "Siro Scraper Bot 🤖",
+ style = MaterialTheme.typography.headlineMedium,
+ fontWeight = FontWeight.Bold
+ )
+ Spacer(modifier = Modifier.height(32.dp))
+ Text(
+ text = "Your Device ID (Android ID):",
+ style = MaterialTheme.typography.bodyLarge
+ )
+ Spacer(modifier = Modifier.height(8.dp))
+ Text(
+ text = deviceId,
+ style = MaterialTheme.typography.headlineSmall,
+ color = MaterialTheme.colorScheme.primary,
+ fontWeight = FontWeight.Black,
+ textAlign = TextAlign.Center
+ )
+ Spacer(modifier = Modifier.height(32.dp))
+ Button(onClick = {
+ val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("Device ID", deviceId)
+ clipboard.setPrimaryClip(clip)
+ Toast.makeText(context, "Copied to clipboard!", Toast.LENGTH_SHORT).show()
+ }) {
+ Text("Copy ID")
+ }
+ Spacer(modifier = Modifier.height(48.dp))
+ Text(
+ text = "Make sure to add this ID to your Siro Server.\nAlso, remember to enable this app in Settings > Accessibility.",
+ style = MaterialTheme.typography.bodyMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.padding(horizontal = 32.dp)
+ )
+ }
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Android_botTheme {
- Greeting("Android")
+ BotSetupScreen("a1b2c3d4e5f6g7h8")
}
}
\ No newline at end of file
diff --git a/android_bot/app/src/main/java/com/siro/android_bot/network/WorkerClient.kt b/android_bot/app/src/main/java/com/siro/android_bot/network/WorkerClient.kt
new file mode 100644
index 0000000..4f01755
--- /dev/null
+++ b/android_bot/app/src/main/java/com/siro/android_bot/network/WorkerClient.kt
@@ -0,0 +1,121 @@
+package com.siro.android_bot.network
+
+import android.content.Context
+import android.provider.Settings
+import android.util.Log
+import org.json.JSONObject
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.io.OutputStreamWriter
+import java.net.HttpURLConnection
+import java.net.URL
+import javax.crypto.Mac
+import javax.crypto.spec.SecretKeySpec
+
+class WorkerClient(private val context: Context) {
+
+ private val TAG = "WorkerClient"
+ private val SECRET_KEY = "SIRO_BOT_SUPER_SECRET_123"
+
+ // Read the real hardware Android ID
+ val deviceId: String by lazy {
+ Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN_DEVICE"
+ }
+
+ // Change this to your actual server domain
+ private val BASE_URL = "https://api.intaleq.xyz/bot_android/standalone_worker.php"
+ // For local testing use: "http://10.0.2.2:8000/standalone_worker.php"
+
+ private fun generateSignature(deviceId: String, ts: Long): String {
+ val message = "$deviceId$ts"
+ val algorithm = "HmacSHA256"
+ val mac = Mac.getInstance(algorithm)
+ val secretKeySpec = SecretKeySpec(SECRET_KEY.toByteArray(), algorithm)
+ mac.init(secretKeySpec)
+ val hashBytes = mac.doFinal(message.toByteArray())
+ return hashBytes.joinToString("") { "%02x".format(it) }
+ }
+
+ fun fetchTask(): JSONObject? {
+ try {
+ val ts = System.currentTimeMillis() / 1000
+ val sig = generateSignature(deviceId, ts)
+
+ val urlString = "$BASE_URL?device_id=$deviceId&ts=$ts&sig=$sig"
+ val url = URL(urlString)
+
+ Log.d(TAG, "Fetching task from: $urlString")
+ val connection = url.openConnection() as HttpURLConnection
+ connection.requestMethod = "GET"
+ connection.connectTimeout = 10000
+ connection.readTimeout = 10000
+
+ val responseCode = connection.responseCode
+ if (responseCode == HttpURLConnection.HTTP_OK) {
+ val reader = BufferedReader(InputStreamReader(connection.inputStream))
+ val response = reader.readText()
+ reader.close()
+ return JSONObject(response)
+ } else {
+ Log.e(TAG, "Fetch Error: HTTP $responseCode")
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception during fetchTask: ${e.message}")
+ }
+ return null
+ }
+
+ fun submitPrice(taskId: String, appName: String, startLat: Double, startLng: Double, endLat: Double, endLng: Double, distanceKm: Double, price: Double): Boolean {
+ try {
+ val ts = System.currentTimeMillis() / 1000
+ val sig = generateSignature(deviceId, ts)
+
+ val payload = JSONObject().apply {
+ put("device_id", deviceId)
+ put("ts", ts)
+ put("sig", sig)
+ put("task_id", taskId)
+ put("type", "price_check")
+ put("status", "success")
+
+ val resultData = JSONObject().apply {
+ put("app", appName)
+ put("distance_km", distanceKm)
+ put("price", price)
+ put("start_lat", startLat)
+ put("start_lng", startLng)
+ put("end_lat", endLat)
+ put("end_lng", endLng)
+ }
+ put("result_data", resultData)
+ }
+
+ val url = URL(BASE_URL)
+ val connection = url.openConnection() as HttpURLConnection
+ connection.requestMethod = "POST"
+ connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8")
+ connection.doOutput = true
+ connection.connectTimeout = 10000
+ connection.readTimeout = 10000
+
+ val writer = OutputStreamWriter(connection.outputStream)
+ writer.write(payload.toString())
+ writer.flush()
+ writer.close()
+
+ val responseCode = connection.responseCode
+ return if (responseCode == HttpURLConnection.HTTP_OK) {
+ val reader = BufferedReader(InputStreamReader(connection.inputStream))
+ Log.d(TAG, "Submit Success: ${reader.readText()}")
+ reader.close()
+ true
+ } else {
+ Log.e(TAG, "Submit Error: HTTP $responseCode")
+ false
+ }
+ } catch (e: Exception) {
+ Log.e(TAG, "Exception during submitPrice: ${e.message}")
+ return false
+ }
+ }
+}
diff --git a/android_bot/app/src/main/java/com/siro/android_bot/service/AppLauncher.kt b/android_bot/app/src/main/java/com/siro/android_bot/service/AppLauncher.kt
new file mode 100644
index 0000000..08c71f6
--- /dev/null
+++ b/android_bot/app/src/main/java/com/siro/android_bot/service/AppLauncher.kt
@@ -0,0 +1,39 @@
+package com.siro.android_bot.service
+
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+
+object AppLauncher {
+ private const val TAG = "AppLauncher"
+
+ // Maps the server app name to the actual Android package name
+ // Important: We will verify these exact package names tomorrow once you install them.
+ private val APP_PACKAGES = mapOf(
+ "yallago" to "ae.com.yalla.go.dubai.client",
+ "ae.com.yalla.go.dubai.client" to "ae.com.yalla.go.dubai.client",
+ "zaken" to "com.zakinn.app",
+ "com.zakinn.app" to "com.zakinn.app",
+ "tufaddal" to "com.bis.taxi",
+ "com.bis.taxi" to "com.bis.taxi"
+ )
+
+ fun launchApp(context: Context, appName: String): Boolean {
+ val packageName = APP_PACKAGES[appName.lowercase()]
+ if (packageName == null) {
+ Log.e(TAG, "Unknown app name from server: $appName")
+ return false
+ }
+
+ val launchIntent = context.packageManager.getLaunchIntentForPackage(packageName)
+ if (launchIntent != null) {
+ launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ context.startActivity(launchIntent)
+ Log.i(TAG, "Successfully launched $appName ($packageName)")
+ return true
+ } else {
+ Log.e(TAG, "App $appName ($packageName) is not installed on this device!")
+ return false
+ }
+ }
+}
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
new file mode 100644
index 0000000..1d7ae8b
--- /dev/null
+++ b/android_bot/app/src/main/java/com/siro/android_bot/service/ScraperAccessibilityService.kt
@@ -0,0 +1,397 @@
+package com.siro.android_bot.service
+
+import android.accessibilityservice.AccessibilityService
+import android.util.Log
+import android.view.accessibility.AccessibilityEvent
+import com.siro.android_bot.network.WorkerClient
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+
+enum class BotState {
+ IDLE,
+ LAUNCHING_APP,
+ SEARCHING_START,
+ SEARCHING_END,
+ READING_PRICE,
+ SUBMITTING
+}
+
+class ScraperAccessibilityService : AccessibilityService() {
+
+ private val TAG = "ScraperService"
+ private var pollingJob: Job? = null
+ private val serviceScope = CoroutineScope(Dispatchers.IO)
+
+ private lateinit var workerClient: WorkerClient
+
+ // State Machine Memory
+ private var currentState = BotState.IDLE
+ private var currentTask: JSONObject? = null
+ private var currentAppName: String = ""
+
+ override fun onServiceConnected() {
+ super.onServiceConnected()
+ Log.i(TAG, "Accessibility Service Connected!")
+
+ workerClient = WorkerClient(this)
+ Log.i(TAG, "Device Android ID: ${workerClient.deviceId}")
+
+ startPolling()
+ }
+
+ private fun startPolling() {
+ pollingJob = serviceScope.launch {
+ while (isActive) {
+ if (currentState == BotState.IDLE) {
+ Log.d(TAG, "Polling for tasks as ${workerClient.deviceId}...")
+ val result = workerClient.fetchTask()
+
+ if (result != null && result.optBoolean("has_task", false)) {
+ val task = result.getJSONObject("task")
+ handleTask(task)
+ } else {
+ Log.d(TAG, "No tasks available.")
+ }
+ }
+ // Poll every 15 seconds
+ delay(15000)
+ }
+ }
+ }
+
+ private fun handleTask(task: JSONObject) {
+ currentTask = task
+ currentAppName = task.optString("app")
+ val taskId = task.optString("task_id")
+
+ Log.i(TAG, "Received Task: $taskId for App: $currentAppName")
+
+ currentState = BotState.LAUNCHING_APP
+
+ // Launch the App
+ val success = AppLauncher.launchApp(this, currentAppName)
+ if (success) {
+ // We wait for the AccessibilityEvent to tell us the app is opened,
+ // but we can preemptively change state to SEARCHING_START
+ currentState = BotState.SEARCHING_START
+ Log.i(TAG, "State -> SEARCHING_START")
+ } else {
+ // Failed to launch (app not installed)
+ Log.e(TAG, "Failed to launch app. Returning to IDLE.")
+ currentState = BotState.IDLE
+ currentTask = null
+ }
+ }
+
+ override fun onAccessibilityEvent(event: AccessibilityEvent?) {
+ if (event == null) return
+
+ val packageName = event.packageName?.toString() ?: return
+ val rootNode = rootInActiveWindow ?: return
+
+ when (packageName) {
+ "ae.com.yalla.go.dubai.client" -> handleYallaGoAutomation(rootNode)
+ "com.zakinn.app" -> handleZakinnAutomation(rootNode)
+ "com.bis.taxi" -> handleTfadalAutomation(rootNode)
+ }
+ }
+
+ private fun handleYallaGoAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) {
+ val task = currentTask ?: return
+
+ when (currentState) {
+ BotState.SEARCHING_START -> {
+ // If on main screen, find the "Where to" or dropoff TextView by text to tap
+ val dropoffNode = findNodeByText(rootNode, "Where to") ?: findNodeByText(rootNode, "أين تريد الذهاب")
+ if (dropoffNode != null) {
+ dropoffNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
+ Log.i(TAG, "YallaGo: Clicked dropoff entry node.")
+ currentState = BotState.SEARCHING_END
+ return
+ }
+
+ // If already on the search screen, type pickup location
+ val startLoc = task.optString("start_location", "Main Square")
+ val pickupEdit = findEditableNode(rootNode)
+ if (pickupEdit != null) {
+ val arguments = android.os.Bundle().apply {
+ putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, startLoc)
+ }
+ pickupEdit.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
+ Log.i(TAG, "YallaGo: Set pickup to: $startLoc")
+ currentState = BotState.SEARCHING_END
+ }
+ }
+ BotState.SEARCHING_END -> {
+ // Find destination field on search screen or select-on-map screen
+ val endLoc = task.optString("end_location", "Airport")
+ val destEdit = findEditableNode(rootNode)
+ if (destEdit != null) {
+ val arguments = android.os.Bundle().apply {
+ putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc)
+ }
+ destEdit.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
+ Log.i(TAG, "YallaGo: Set destination to: $endLoc")
+
+ // Look for the primary action button to confirm (e.g., "Confirm", "Book", "Done")
+ val confirmBtn = findNodeByText(rootNode, "Confirm") ?: findNodeByText(rootNode, "تأكيد")
+ if (confirmBtn != null) {
+ confirmBtn.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
+ Log.i(TAG, "YallaGo: Clicked confirm button.")
+ currentState = BotState.READING_PRICE
+ }
+ }
+ }
+ BotState.READING_PRICE -> {
+ // Search the screen for currency value
+ searchPriceByCurrency(rootNode)
+ }
+ else -> {}
+ }
+ }
+
+ private fun handleZakinnAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) {
+ val task = currentTask ?: return
+
+ when (currentState) {
+ BotState.SEARCHING_START -> {
+ val inputNodes = rootNode.findAccessibilityNodeInfosByViewId("com.zakinn.app:id/ui-input-taxi-startText")
+ if (inputNodes.isNotEmpty()) {
+ val startLoc = task.optString("start_location", "Main Square")
+ val arguments = android.os.Bundle().apply {
+ putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, startLoc)
+ }
+ inputNodes[0].performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
+ Log.i(TAG, "Zakinn: Entered start location: $startLoc")
+ currentState = BotState.SEARCHING_END
+ } else {
+ traverseAndType(rootNode, "locTypeStart", task.optString("start_location"))
+ }
+ }
+ BotState.SEARCHING_END -> {
+ val confirmButton = rootNode.findAccessibilityNodeInfosByViewId("com.zakinn.app:id/ui-btn-checkout-order-confirm")
+ if (confirmButton.isNotEmpty()) {
+ confirmButton[0].performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
+ Log.i(TAG, "Zakinn: Confirmed checkout.")
+ currentState = BotState.READING_PRICE
+ } else {
+ val fallbackConfirm = findNodeByText(rootNode, "Confirm") ?: findNodeByText(rootNode, "تأكيد")
+ if (fallbackConfirm != null) {
+ fallbackConfirm.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
+ Log.i(TAG, "Zakinn: Confirmed checkout via fallback text.")
+ currentState = BotState.READING_PRICE
+ }
+ }
+ }
+ BotState.READING_PRICE -> {
+ val priceValueNodes = rootNode.findAccessibilityNodeInfosByViewId("com.zakinn.app:id/service-price-value")
+ if (priceValueNodes.isNotEmpty()) {
+ val price = priceValueNodes[0].text?.toString() ?: ""
+ Log.i(TAG, "Zakinn: Read price: $price")
+ submitPriceToServer(price)
+ } else {
+ searchPriceByCurrency(rootNode)
+ }
+ }
+ else -> {}
+ }
+ }
+
+ private fun handleTfadalAutomation(rootNode: android.view.accessibility.AccessibilityNodeInfo) {
+ val task = currentTask ?: return
+
+ when (currentState) {
+ BotState.SEARCHING_START -> {
+ // In Flutter, look for inputs matching localized source strings
+ val sourceNode = findNodeByText(rootNode, "Find Your Source")
+ ?: findNodeByText(rootNode, "Source")
+ ?: findNodeByText(rootNode, "نقطة الانطلاق")
+ ?: findNodeByText(rootNode, "الانطلاق")
+ ?: findNodeByText(rootNode, "Ø§Ø¨ØØ« عن مصدرك")
+ ?: findNodeByText(rootNode, "Kalkış")
+ ?: findNodeByText(rootNode, "Kaynağınızı bulun")
+
+ if (sourceNode != null) {
+ val startLoc = task.optString("start_location", "Main Square")
+ val arguments = android.os.Bundle().apply {
+ putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, startLoc)
+ }
+ sourceNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
+ Log.i(TAG, "Tfadal: Set source location: $startLoc")
+ currentState = BotState.SEARCHING_END
+ }
+ }
+ BotState.SEARCHING_END -> {
+ val destNode = findNodeByText(rootNode, "Find Your Destination")
+ ?: findNodeByText(rootNode, "Destination")
+ ?: findNodeByText(rootNode, "الوجهة")
+ ?: findNodeByText(rootNode, "Ø§Ø¨ØØ« عن وجهتك")
+ ?: findNodeByText(rootNode, "Varış")
+ ?: findNodeByText(rootNode, "Varış noktanızı bulun")
+
+ if (destNode != null) {
+ val endLoc = task.optString("end_location", "Airport")
+ val arguments = android.os.Bundle().apply {
+ putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, endLoc)
+ }
+ destNode.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
+ Log.i(TAG, "Tfadal: Set destination location: $endLoc")
+
+ val requestButton = findNodeByText(rootNode, "Add a trip")
+ ?: findNodeByText(rootNode, "Normal Trip")
+ ?: findNodeByText(rootNode, "طلب الرØÙ„Ø©")
+ ?: findNodeByText(rootNode, "اطلب رØÙ„Ø©")
+ ?: findNodeByText(rootNode, "Normal Yolculuk")
+ ?: findNodeByText(rootNode, "Yolculuk ekle")
+ ?: findNodeByText(rootNode, "Continue")
+ ?: findNodeByText(rootNode, "متابعة")
+ ?: findNodeByText(rootNode, "Devam")
+
+ if (requestButton != null) {
+ requestButton.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK)
+ Log.i(TAG, "Tfadal: Clicked request trip button.")
+ currentState = BotState.READING_PRICE
+ }
+ }
+ }
+ BotState.READING_PRICE -> {
+ // Read expected price using localized strings
+ val priceNode = findNodeByText(rootNode, "Expected Price")
+ ?: findNodeByText(rootNode, "Total Price")
+ ?: findNodeByText(rootNode, "السعر المتوقع")
+ ?: findNodeByText(rootNode, "السعر الإجمالي")
+
+ if (priceNode != null) {
+ val priceText = priceNode.text?.toString() ?: ""
+ Log.i(TAG, "Tfadal: Found price header node: $priceText")
+ searchPriceByCurrency(rootNode)
+ } else {
+ searchPriceByCurrency(rootNode)
+ }
+ }
+ else -> {}
+ }
+ }
+
+ private fun findNodeByText(node: android.view.accessibility.AccessibilityNodeInfo?, query: String): android.view.accessibility.AccessibilityNodeInfo? {
+ if (node == null) return null
+ val text = node.text?.toString() ?: ""
+ val desc = node.contentDescription?.toString() ?: ""
+ if (text.contains(query, ignoreCase = true) || desc.contains(query, ignoreCase = true)) {
+ return node
+ }
+ for (i in 0 until node.childCount) {
+ val res = findNodeByText(node.getChild(i), query)
+ if (res != null) return res
+ }
+ return null
+ }
+
+ private fun findEditableNode(node: android.view.accessibility.AccessibilityNodeInfo?): android.view.accessibility.AccessibilityNodeInfo? {
+ if (node == null) return null
+ if (node.className == "android.widget.EditText" || node.isEditable) {
+ return node
+ }
+ for (i in 0 until node.childCount) {
+ val res = findEditableNode(node.getChild(i))
+ if (res != null) return res
+ }
+ return null
+ }
+
+ private fun traverseAndType(node: android.view.accessibility.AccessibilityNodeInfo?, hintText: String, value: String) {
+ if (node == null) return
+ if (node.className == "android.widget.EditText" || node.isEditable) {
+ val arguments = android.os.Bundle().apply {
+ putCharSequence(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, value)
+ }
+ node.performAction(android.view.accessibility.AccessibilityNodeInfo.ACTION_SET_TEXT, arguments)
+ Log.i(TAG, "Typed '$value' into editable node.")
+ return
+ }
+ for (i in 0 until node.childCount) {
+ traverseAndType(node.getChild(i), hintText, value)
+ }
+ }
+
+ private fun searchPriceByCurrency(node: android.view.accessibility.AccessibilityNodeInfo?) {
+ if (node == null) return
+ val text = node.text?.toString() ?: ""
+ if (text.contains("ل.س") || text.contains("SYP") || text.contains("AED") || text.contains("SP") || text.contains("SP.")) {
+ Log.i(TAG, "Found price pattern dynamically: $text")
+ submitPriceToServer(text)
+ return
+ }
+ for (i in 0 until node.childCount) {
+ searchPriceByCurrency(node.getChild(i))
+ }
+ }
+
+ private fun submitPriceToServer(rawPrice: String) {
+ 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
+
+ // 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 {
+ Log.i(TAG, "Submitting price $numericPrice for task $taskId...")
+ val success = workerClient.submitPrice(
+ taskId = taskId,
+ appName = currentAppName,
+ startLat = startLat,
+ startLng = startLng,
+ endLat = endLat,
+ endLng = endLng,
+ distanceKm = distanceKm,
+ price = numericPrice
+ )
+
+ if (success) {
+ Log.i(TAG, "Successfully submitted price to server.")
+ } else {
+ Log.e(TAG, "Failed to submit price.")
+ }
+
+ // Go back to IDLE
+ currentState = BotState.IDLE
+ currentTask = null
+ }
+ }
+
+ private fun calculateDistanceInKm(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
+ val r = 6371.0 // Earth radius in km
+ val dLat = Math.toRadians(lat2 - lat1)
+ val dLon = Math.toRadians(lon2 - lon1)
+ val a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2)
+ val c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a))
+ return r * c
+ }
+
+ override fun onInterrupt() {
+ Log.w(TAG, "Accessibility Service Interrupted")
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ pollingJob?.cancel()
+ Log.i(TAG, "Accessibility Service Destroyed")
+ }
+}
diff --git a/android_bot/app/src/main/res/values/strings.xml b/android_bot/app/src/main/res/values/strings.xml
index 0b781ab..bc4152e 100644
--- a/android_bot/app/src/main/res/values/strings.xml
+++ b/android_bot/app/src/main/res/values/strings.xml
@@ -1,3 +1,4 @@
android_bot
+ Siro Scraper Accessibility Service helps automate competitor price checks.
\ No newline at end of file
diff --git a/android_bot/app/src/main/res/xml/accessibility_service_config.xml b/android_bot/app/src/main/res/xml/accessibility_service_config.xml
new file mode 100644
index 0000000..cf6c11d
--- /dev/null
+++ b/android_bot/app/src/main/res/xml/accessibility_service_config.xml
@@ -0,0 +1,9 @@
+
+
diff --git a/backend/bot/standalone_worker.php b/backend/bot/standalone_worker.php
new file mode 100644
index 0000000..fd03db7
--- /dev/null
+++ b/backend/bot/standalone_worker.php
@@ -0,0 +1,753 @@
+ 900) {
+ return false;
+ }
+ // Generate the expected signature
+ $expected_sig = hash_hmac('sha256', $device_id . $ts, $secret_key);
+ // Secure comparison
+ return hash_equals($expected_sig, $sig);
+}
+
+function jsonError($message, $code = 400) {
+ http_response_code($code);
+ echo json_encode(['status' => 'failure', 'message' => $message]);
+ exit;
+}
+
+// ----------------------------------------------------------------------------
+// API Request Routing
+// ----------------------------------------------------------------------------
+$method = $_SERVER['REQUEST_METHOD'];
+$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
+
+// Check if request is API call from the Bot
+$isApiCall = false;
+$device_id = null;
+$ts = null;
+$sig = null;
+
+if ($method === 'GET' && isset($_GET['device_id'], $_GET['ts'], $_GET['sig'])) {
+ $isApiCall = true;
+ $device_id = $_GET['device_id'];
+ $ts = intval($_GET['ts']);
+ $sig = $_GET['sig'];
+} elseif ($method === 'POST') {
+ // Check if JSON body has device credentials
+ $rawInput = file_get_contents('php://input');
+ $input = json_decode($rawInput, true);
+ if ($input && isset($input['device_id'], $input['ts'], $input['sig'])) {
+ $isApiCall = true;
+ $device_id = $input['device_id'];
+ $ts = intval($input['ts']);
+ $sig = $input['sig'];
+ }
+}
+
+if ($isApiCall) {
+ header('Content-Type: application/json; charset=UTF-8');
+
+ // Validate signature
+ if (!validateSignature($device_id, $ts, $sig, SECRET_KEY)) {
+ jsonError("Unauthorized device signature or expired timestamp. Device ID: $device_id, ts: $ts", 401);
+ }
+
+ if ($method === 'GET') {
+ // Dequeue one task for this bot (FIFO)
+ $tasks = json_decode(file_get_contents(TASKS_FILE), true);
+
+ if (count($tasks) > 0) {
+ $task = array_shift($tasks);
+ file_put_contents(TASKS_FILE, json_encode($tasks, JSON_PRETTY_PRINT));
+
+ echo json_encode([
+ "status" => "success",
+ "has_task" => true,
+ "task" => $task
+ ]);
+ } else {
+ echo json_encode([
+ "status" => "success",
+ "has_task" => false
+ ]);
+ }
+ exit;
+ } elseif ($method === 'POST') {
+ // Record Scrape result
+ if (empty($input['task_id']) || empty($input['status'])) {
+ jsonError("Missing required parameters in payload");
+ }
+
+ $results = json_decode(file_get_contents(RESULTS_FILE), true);
+
+ $newResult = [
+ 'task_id' => $input['task_id'],
+ 'device_id' => $device_id,
+ 'status' => $input['status'], // 'success' or 'failed'
+ 'type' => $input['type'] ?? 'price_check',
+ 'recorded_at' => date('Y-m-d H:i:s'),
+ 'result_data' => $input['result_data'] ?? []
+ ];
+
+ array_unshift($results, $newResult);
+ file_put_contents(RESULTS_FILE, json_encode($results, JSON_PRETTY_PRINT));
+
+ echo json_encode([
+ "status" => "success",
+ "message" => "Result recorded successfully"
+ ]);
+ exit;
+ }
+}
+
+// ----------------------------------------------------------------------------
+// Admin Dashboard UI (Rendered on normal GET browser requests)
+// ----------------------------------------------------------------------------
+
+// Handle Admin Actions
+$message = '';
+$msgType = 'success';
+
+if (isset($_POST['action'])) {
+ if ($_POST['action'] === 'add_task') {
+ $app = $_POST['app'] ?? '';
+ $start_loc = $_POST['start_location'] ?? '';
+ $end_loc = $_POST['end_location'] ?? '';
+ $start_lat = floatval($_POST['start_lat'] ?? 0.0);
+ $start_lng = floatval($_POST['start_lng'] ?? 0.0);
+ $end_lat = floatval($_POST['end_lat'] ?? 0.0);
+ $end_lng = floatval($_POST['end_lng'] ?? 0.0);
+
+ if ($app && $start_loc && $end_loc) {
+ $tasks = json_decode(file_get_contents(TASKS_FILE), true);
+ $taskId = "prc_" . uniqid();
+
+ $newTask = [
+ "task_id" => $taskId,
+ "type" => "price_check",
+ "app" => $app,
+ "start_location" => $start_loc,
+ "end_location" => $end_loc,
+ "payload" => [
+ "start_lat" => $start_lat,
+ "start_lng" => $start_lng,
+ "end_lat" => $end_lat,
+ "end_lng" => $end_lng
+ ]
+ ];
+
+ $tasks[] = $newTask;
+ file_put_contents(TASKS_FILE, json_encode($tasks, JSON_PRETTY_PRINT));
+ $message = "Task successfully added and queued! Task ID: $taskId";
+ } else {
+ $message = "Please fill in all required fields.";
+ $msgType = 'error';
+ }
+ } elseif ($_POST['action'] === 'clear_tasks') {
+ file_put_contents(TASKS_FILE, json_encode([]));
+ $message = "Task queue cleared successfully.";
+ } elseif ($_POST['action'] === 'clear_results') {
+ file_put_contents(RESULTS_FILE, json_encode([]));
+ $message = "Scrape results history cleared successfully.";
+ }
+}
+
+// Fetch stats and lists for display
+$currentTasks = json_decode(file_get_contents(TASKS_FILE), true);
+$scrapedResults = json_decode(file_get_contents(RESULTS_FILE), true);
+
+?>
+
+
+
+
+
+ Siro Bot - Standalone Server Control Panel
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Queued task list (Sent to bot via GET request)
+ 0): ?>
+
+
+
+
+
+
+
No pending tasks. Queue some tasks using the form on the left!
+
+
+
+
+
+
+
+
+
+
+
+ Results submitted by Bot via POST request
+ 0): ?>
+
+
+
+
+
+
+
No scraping results recorded yet. Results will appear here when the Android bot submits them.
+
+
+
+
+
+
+ Task ID:
+
+
Device:
+
Distance: km | Time:
+
+
+
+ SYP
+
+ Scrape Failed
+
+
+
+
+
+
+
+
+
+
+
+
+
+