Update: 2026-05-15 03:09:36

This commit is contained in:
Hamza-Ayed
2026-05-15 03:09:36 +03:00
parent 9fc1319946
commit dd4fcb9bee
93 changed files with 766 additions and 55 deletions

View File

@@ -21,6 +21,10 @@ import androidx.core.content.ContextCompat
import com.jordanbot.autoride.service.BotForegroundService
import com.jordanbot.autoride.service.OverlayService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.jordanbot.autoride.subscription.SubscriptionManager
class MainActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
@@ -31,10 +35,22 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Load cached subscription data
SubscriptionManager.loadLocalCache(this)
// Check subscription with server
lifecycleScope.launch {
SubscriptionManager.checkSubscription(this@MainActivity)
}
tvStatus = findViewById(R.id.tv_status)
btnStart = findViewById(R.id.btn_start)
btnStop = findViewById(R.id.btn_stop)
findViewById<Button>(R.id.btn_subscriptions).setOnClickListener {
startActivity(Intent(this, SubscriptionActivity::class.java))
}
findViewById<Button>(R.id.btn_notification).setOnClickListener {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
}

View File

@@ -0,0 +1,78 @@
package com.jordanbot.autoride
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.jordanbot.autoride.api.ActivateSubscriptionRequest
import com.jordanbot.autoride.api.ApiClient
import com.jordanbot.autoride.subscription.SubscriptionManager
import com.jordanbot.autoride.utils.DeviceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class SubscriptionActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView
private lateinit var btnBasic: Button
private lateinit var btnPro: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_subscription)
tvStatus = findViewById(R.id.tv_current_status)
btnBasic = findViewById(R.id.btn_subscribe_basic)
btnPro = findViewById(R.id.btn_subscribe_pro)
updateStatusUI()
btnBasic.setOnClickListener { activatePlan("basic") }
btnPro.setOnClickListener { activatePlan("pro") }
}
private fun updateStatusUI() {
val plan = SubscriptionManager.currentPlan
val limit = SubscriptionManager.ridesLimit
val today = SubscriptionManager.ridesToday
val planText = when(plan) {
"basic" -> "أساسي ($limit طلب / يوم)"
"pro" -> "احترافي (لا محدود)"
"annual" -> "سنوي (لا محدود)"
else -> "مجاني (1 طلب / يوم)"
}
tvStatus.text = "الخطة الحالية: $planText\nاستهلاك اليوم: $today"
}
private fun activatePlan(plan: String) {
// In a real app, integrate payment gateway here.
// For demonstration, we just call the API directly.
val fingerprint = DeviceUtils.getDeviceFingerprint(this)
lifecycleScope.launch {
try {
val response = withContext(Dispatchers.IO) {
ApiClient.service.activateSubscription(
ActivateSubscriptionRequest(fingerprint, plan, "DEMO_REF_123")
)
}
if (response.success) {
Toast.makeText(this@SubscriptionActivity, "تم تفعيل الاشتراك بنجاح!", Toast.LENGTH_SHORT).show()
// Re-check subscription to update local cache
SubscriptionManager.checkSubscription(this@SubscriptionActivity)
updateStatusUI()
} else {
Toast.makeText(this@SubscriptionActivity, "فشل تفعيل الاشتراك: ${response.message}", Toast.LENGTH_SHORT).show()
}
} catch (e: Exception) {
Toast.makeText(this@SubscriptionActivity, "حدث خطأ في الاتصال", Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@@ -38,12 +38,41 @@ data class ApiResponse(
val message: String
)
data class CheckSubscriptionRequest(val fingerprint: String)
data class CheckSubscriptionResponse(
val success: Boolean,
val plan: String?,
val expires_at: String?,
val rides_today: Int?,
val rides_limit: Int?,
val can_accept: Boolean?
)
data class ActivateSubscriptionRequest(
val fingerprint: String,
val plan: String,
val payment_ref: String?
)
data class ActivateSubscriptionResponse(
val success: Boolean,
val message: String?,
val plan: String?,
val expires_at: String?
)
interface BackendApiService {
@POST("api/rides.php")
suspend fun logRide(@Body request: RideLogRequest): ApiResponse
@POST("api/location.php")
suspend fun updateBulkLocation(@Body request: BulkLocationRequest): ApiResponse
@POST("api/subscription/check.php")
suspend fun checkSubscription(@Body request: CheckSubscriptionRequest): CheckSubscriptionResponse
@POST("api/subscription/activate.php")
suspend fun activateSubscription(@Body request: ActivateSubscriptionRequest): ActivateSubscriptionResponse
}
object ApiClient {

View File

@@ -0,0 +1,67 @@
package com.jordanbot.autoride.engine
import android.util.Log
import com.jordanbot.autoride.model.RideRequest
import kotlinx.coroutines.*
object RideDataMerger {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var lastRequest: RideRequest? = null
private var lastTimestamp: Long = 0
private const val MERGE_WINDOW_MS = 5000L
var onRideReady: ((RideRequest) -> Unit)? = null
@Synchronized
fun updateFromNotification(request: RideRequest) {
Log.d("JordanBot", "🔔 Merger received Notification data: ${request.priceJod} JOD")
merge(request)
}
@Synchronized
fun updateFromScreen(request: RideRequest) {
Log.d("JordanBot", "👁️ Merger received Screen data: ${request.priceJod} JOD")
merge(request)
}
private fun merge(newPart: RideRequest) {
val now = System.currentTimeMillis()
val current = lastRequest
if (current == null || (now - lastTimestamp) > MERGE_WINDOW_MS || current.appPackage != newPart.appPackage) {
// New request sequence
lastRequest = newPart
lastTimestamp = now
// Wait a bit to see if more data comes from the other source before emitting
scope.launch {
delay(1500) // Wait 1.5s for the other source
emitIfReady()
}
} else {
// Merge into current
lastRequest = current.copy(
priceJod = newPart.priceJod ?: current.priceJod,
minutesAway = newPart.minutesAway ?: current.minutesAway,
distanceKm = newPart.distanceKm ?: current.distanceKm,
pickupAddress = newPart.pickupAddress ?: current.pickupAddress,
dropoffAddress = newPart.dropoffAddress ?: current.dropoffAddress,
title = if (newPart.title.isNotEmpty() && newPart.title != "Screen Scrape") newPart.title else current.title,
text = if (newPart.text.isNotEmpty() && newPart.text != current.text) "${current.text} | ${newPart.text}" else current.text
)
lastTimestamp = now
emitIfReady()
}
}
private fun emitIfReady() {
val ride = lastRequest ?: return
// Criteria for "Ready": We have at least a price
if (ride.priceJod != null) {
Log.d("JordanBot", "🔀 MERGED RIDE READY: $ride")
onRideReady?.invoke(ride)
lastRequest = null // Clear to avoid double emission
}
}
}

View File

@@ -5,6 +5,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.location.Location
import android.os.Build
import android.os.IBinder
import android.os.Looper
@@ -24,13 +25,26 @@ class BotForegroundService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private lateinit var fusedLocationClient: FusedLocationProviderClient
private val locationBuffer = mutableListOf<LocationPoint>()
private val UPLOAD_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30)
private val TRACK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(3)
// Battery-optimized intervals
private val UPLOAD_INTERVAL_MS = TimeUnit.MINUTES.toMillis(5) // Upload every 5 minutes
private val TRACK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(15) // GPS poll every 15 seconds
private val MIN_DISPLACEMENT_METERS = 15f // Ignore movement < 15m
// Displacement filter: track last known location
private var lastTrackedLocation: Location? = null
private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location ->
// Displacement filter: skip if driver hasn't moved 15 meters
val last = lastTrackedLocation
if (last != null && last.distanceTo(location) < MIN_DISPLACEMENT_METERS) {
return // Driver is stationary, save battery
}
lastTrackedLocation = location
val point = LocationPoint(
latitude = location.latitude,
longitude = location.longitude,
@@ -58,7 +72,7 @@ class BotForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Jordan Bot يعمل حالياً")
.setContentText("نحن نراقب الإشعارات ونحدث الخرائط.")
.setContentText("نبحث عن طلبات قريبة منك...")
.setSmallIcon(R.drawable.bg_bubble)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
@@ -69,7 +83,10 @@ class BotForegroundService : Service() {
@SuppressLint("MissingPermission")
private fun startTracking() {
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, TRACK_INTERVAL_MS)
val locationRequest = LocationRequest.Builder(
Priority.PRIORITY_BALANCED_POWER_ACCURACY, TRACK_INTERVAL_MS
)
.setMinUpdateDistanceMeters(MIN_DISPLACEMENT_METERS)
.setMinUpdateIntervalMillis(TRACK_INTERVAL_MS)
.build()

View File

@@ -2,70 +2,143 @@ package com.jordanbot.autoride.service
import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import com.jordanbot.autoride.engine.RideDataMerger
import com.jordanbot.autoride.filter.FilterEngine
import com.jordanbot.autoride.model.RideRequest
class RideAccessibilityService : AccessibilityService() {
private val handler = Handler(Looper.getMainLooper())
private val filterEngine = FilterEngine()
override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
val rootNode = rootInActiveWindow ?: return
val packageName = event.packageName?.toString() ?: ""
val source = event.source ?: return
val packageName = event.packageName?.toString() ?: return
// Only process supported apps
if (!isSupportedApp(packageName)) return
// Check if this package was recently flagged by NotificationListener to be accepted
if (RideNotificationListener.pendingAcceptPackage == packageName) {
val timeSinceRequest = System.currentTimeMillis() - RideNotificationListener.lastRequestTime
if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
// Only accept if within 15 seconds of notification
if (timeSinceRequest < 15000) {
findAndClickAcceptButton(source)
} else {
RideNotificationListener.pendingAcceptPackage = null // Expired
val screenData = scrapeScreen(rootNode, packageName)
if (screenData != null) {
RideDataMerger.updateFromScreen(screenData)
}
}
}
private fun findAndClickAcceptButton(rootNode: AccessibilityNodeInfo) {
// Arabic and English accept terms
val acceptTerms = listOf("Accept", "قبول", "Tap to accept", "اضغط للقبول")
private fun isSupportedApp(pkg: String): Boolean {
return pkg == "com.ubercab.driver" || pkg == "com.careem.adma" || pkg == "me.jeeny.driver"
}
private fun scrapeScreen(root: AccessibilityNodeInfo, pkg: String): RideRequest? {
val allText = mutableListOf<String>()
collectAllText(root, allText)
val combinedText = allText.joinToString(" ")
// Extract price
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ|دينار)""".toRegex()
val priceMatch = priceRegex.find(combinedText)
val price = priceMatch?.groupValues?.get(1)?.toDoubleOrNull()
if (price != null) {
Log.d("JordanBot", "👁️ Scraped price from screen: $price $pkg")
return RideRequest(
appPackage = pkg,
priceJod = price,
minutesAway = null,
distanceKm = null,
title = "Screen Scrape",
text = combinedText
)
}
return null
}
private fun collectAllText(node: AccessibilityNodeInfo?, list: MutableList<String>) {
if (node == null) return
node.text?.let { list.add(it.toString()) }
for (i in 0 until node.childCount) {
collectAllText(node.getChild(i), list)
}
}
private fun findAndAccept(root: AccessibilityNodeInfo, pkg: String) {
val acceptTerms = listOf("Accept", "قبول", "Tap to accept", "Confirm", "تأكيد")
for (term in acceptTerms) {
val nodes = rootNode.findAccessibilityNodeInfosByText(term)
val nodes = root.findAccessibilityNodeInfosByText(term)
for (node in nodes) {
if (node.isClickable) {
// Simulating human delay (e.g., 500ms)
handler.postDelayed({
node.performAction(AccessibilityNodeInfo.ACTION_CLICK)
Log.d("JordanBot", "Clicked Accept on node: ${node.text}")
// Reset pending accept after clicking
RideNotificationListener.pendingAcceptPackage = null
}, 500)
if (node.isClickable || node.parent?.isClickable == true) {
val target = if (node.isClickable) node else node.parent
Log.d("JordanBot", "🤖 Auto-Accepting via Click: $term")
target.performAction(AccessibilityNodeInfo.ACTION_CLICK)
return
}
}
}
if (pkg == "com.ubercab.driver") {
performUberSwipe()
}
}
override fun onInterrupt() {
Log.e("JordanBot", "Accessibility Service Interrupted")
private fun performUberSwipe() {
Log.d("JordanBot", "🤖 Attempting Uber Swipe...")
val displayMetrics = resources.displayMetrics
val width = displayMetrics.widthPixels
val height = displayMetrics.heightPixels
val path = Path()
path.moveTo(width * 0.2f, height * 0.85f)
path.lineTo(width * 0.8f, height * 0.85f)
val gesture = GestureDescription.Builder()
.addStroke(GestureDescription.StrokeDescription(path, 0, 500))
.build()
dispatchGesture(gesture, null, null)
}
override fun onInterrupt() {}
override fun onServiceConnected() {
super.onServiceConnected()
Log.d("JordanBot", "Accessibility Service Connected")
RideDataMerger.onRideReady = { mergedRide ->
val passesFilters = filterEngine.evaluate(mergedRide)
val canAccept = SubscriptionManager.canAcceptRides
Log.d("JordanBot", "🏁 Merger callback: PassesFilters=$passesFilters, CanAcceptQuota=$canAccept for ${mergedRide.appPackage}")
if (passesFilters) {
if (canAccept) {
val rootNode = rootInActiveWindow
if (rootNode != null) {
findAndAccept(rootNode, mergedRide.appPackage)
} else {
Log.e("JordanBot", "Cannot accept: rootInActiveWindow is null")
}
} else {
Log.w("JordanBot", "⚠️ Ride matches filters but DAILY LIMIT REACHED. Upgrade required.")
}
}
}
val info = AccessibilityServiceInfo().apply {
eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
notificationTimeout = 100
flags = AccessibilityServiceInfo.FLAG_REPORT_VIEW_IDS or AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS
flags = AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS or AccessibilityServiceInfo.FLAG_INCLUDE_NOT_IMPORTANT_VIEWS
}
this.serviceInfo = info
}

View File

@@ -70,23 +70,11 @@ class RideNotificationListener : NotificationListenerService() {
Log.d("JordanBot", "Notification Content: $title | $text")
val rideRequest = parser.parse(title, text)
if (rideRequest == null) {
Log.d("JordanBot", "Parser failed to extract ride info from: $title | $text")
}
if (rideRequest != null) {
val isAccepted = filterEngine.evaluate(rideRequest)
// Send to Backend for Data Mining
sendLogToBackend(rideRequest, isAccepted, "$title - $text")
if (isAccepted) {
Log.d("JordanBot", "Ride ACCEPTED by filter: $rideRequest")
pendingAcceptPackage = packageName
lastRequestTime = System.currentTimeMillis()
} else {
Log.d("JordanBot", "Ride REJECTED by filter: $rideRequest")
}
// Forward to Merger instead of processing here
RideDataMerger.updateFromNotification(rideRequest)
} else {
Log.d("JordanBot", "Parser failed to extract ride info from: $title | $text")
}
}

View File

@@ -0,0 +1,61 @@
package com.jordanbot.autoride.subscription
import android.content.Context
import android.util.Log
import com.jordanbot.autoride.api.ApiClient
import com.jordanbot.autoride.api.CheckSubscriptionRequest
import com.jordanbot.autoride.utils.DeviceUtils
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object SubscriptionManager {
private const val PREFS_NAME = "SubscriptionPrefs"
private const val KEY_PLAN = "plan"
private const val KEY_CAN_ACCEPT = "can_accept"
private const val KEY_RIDES_TODAY = "rides_today"
private const val KEY_RIDES_LIMIT = "rides_limit"
var currentPlan: String = "free"
var canAcceptRides: Boolean = true
var ridesToday: Int = 0
var ridesLimit: Int = 1
fun loadLocalCache(context: Context) {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
currentPlan = prefs.getString(KEY_PLAN, "free") ?: "free"
canAcceptRides = prefs.getBoolean(KEY_CAN_ACCEPT, true)
ridesToday = prefs.getInt(KEY_RIDES_TODAY, 0)
ridesLimit = prefs.getInt(KEY_RIDES_LIMIT, 1)
}
suspend fun checkSubscription(context: Context): Boolean {
return withContext(Dispatchers.IO) {
try {
val fingerprint = DeviceUtils.getDeviceFingerprint(context)
val response = ApiClient.service.checkSubscription(CheckSubscriptionRequest(fingerprint))
if (response.success) {
currentPlan = response.plan ?: "free"
canAcceptRides = response.can_accept ?: false
ridesToday = response.rides_today ?: 0
ridesLimit = response.rides_limit ?: 1
// Save to cache
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
putString(KEY_PLAN, currentPlan)
putBoolean(KEY_CAN_ACCEPT, canAcceptRides)
putInt(KEY_RIDES_TODAY, ridesToday)
putInt(KEY_RIDES_LIMIT, ridesLimit)
apply()
}
Log.d("JordanBot", "Subscription checked: $currentPlan, canAccept: $canAcceptRides")
return@withContext true
}
} catch (e: Exception) {
Log.e("JordanBot", "Failed to check subscription", e)
}
return@withContext false
}
}
}