feat: implement accessibility-based scraper service and standalone worker backend with device registration UI

This commit is contained in:
Hamza-Ayed
2026-06-21 15:21:16 +03:00
parent ce6f22dc71
commit b492b5076b
21 changed files with 1402 additions and 9 deletions

View File

@@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -10,7 +13,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Android_bot">
android:theme="@style/Theme.Android_bot"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -22,6 +26,18 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".service.ScraperAccessibilityService"
android:exported="true"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>
<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>
</application>
</manifest>

View File

@@ -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")
}
}

View File

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

View File

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

View File

@@ -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")
}
}

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name">android_bot</string>
<string name="accessibility_service_description">Siro Scraper Accessibility Service helps automate competitor price checks.</string>
</resources>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
android:accessibilityEventTypes="typeWindowStateChanged|typeWindowContentChanged"
android:accessibilityFeedbackType="feedbackGeneric"
android:accessibilityFlags="flagDefault|flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"
android:canPerformGestures="true"
android:description="@string/accessibility_service_description"
android:notificationTimeout="100" />