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

@@ -1,4 +1,4 @@
#Thu May 14 20:41:24 EET 2026 #Thu May 14 20:47:58 EET 2026
base.0=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeExtDexDebug/classes.dex base.0=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeExtDexDebug/classes.dex
base.1=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeProjectDexDebug/0/classes.dex base.1=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeProjectDexDebug/0/classes.dex
base.2=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeProjectDexDebug/12/classes.dex base.2=/Users/hamzaaleghwairyeen/development/App/jordan_bot/app/build/intermediates/dex/debug/mergeProjectDexDebug/12/classes.dex

View File

@@ -1 +1 @@
Α·ΟύΊΛΊ<EFBFBD><EFBFBD>ΰ ֱ·ֿ÷ֻ÷ׂ<EFBFBD>את

View File

@@ -43,6 +43,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".SubscriptionActivity" android:exported="false" />
<service <service
android:name=".service.RideNotificationListener" android:name=".service.RideNotificationListener"
android:label="@string/app_name" android:label="@string/app_name"

View File

@@ -21,6 +21,10 @@ import androidx.core.content.ContextCompat
import com.jordanbot.autoride.service.BotForegroundService import com.jordanbot.autoride.service.BotForegroundService
import com.jordanbot.autoride.service.OverlayService import com.jordanbot.autoride.service.OverlayService
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import com.jordanbot.autoride.subscription.SubscriptionManager
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private lateinit var tvStatus: TextView private lateinit var tvStatus: TextView
@@ -31,10 +35,22 @@ class MainActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main) 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) tvStatus = findViewById(R.id.tv_status)
btnStart = findViewById(R.id.btn_start) btnStart = findViewById(R.id.btn_start)
btnStop = findViewById(R.id.btn_stop) 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 { findViewById<Button>(R.id.btn_notification).setOnClickListener {
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS)) 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 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 { interface BackendApiService {
@POST("api/rides.php") @POST("api/rides.php")
suspend fun logRide(@Body request: RideLogRequest): ApiResponse suspend fun logRide(@Body request: RideLogRequest): ApiResponse
@POST("api/location.php") @POST("api/location.php")
suspend fun updateBulkLocation(@Body request: BulkLocationRequest): ApiResponse 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 { 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.NotificationManager
import android.app.Service import android.app.Service
import android.content.Intent import android.content.Intent
import android.location.Location
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.Looper import android.os.Looper
@@ -24,13 +25,26 @@ class BotForegroundService : Service() {
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
private lateinit var fusedLocationClient: FusedLocationProviderClient private lateinit var fusedLocationClient: FusedLocationProviderClient
private val locationBuffer = mutableListOf<LocationPoint>() private val locationBuffer = mutableListOf<LocationPoint>()
private val UPLOAD_INTERVAL_MS = TimeUnit.SECONDS.toMillis(30) // Battery-optimized intervals
private val TRACK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(3) 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() { private val locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) { override fun onLocationResult(locationResult: LocationResult) {
locationResult.lastLocation?.let { location -> 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( val point = LocationPoint(
latitude = location.latitude, latitude = location.latitude,
longitude = location.longitude, longitude = location.longitude,
@@ -58,7 +72,7 @@ class BotForegroundService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification = NotificationCompat.Builder(this, CHANNEL_ID) val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Jordan Bot يعمل حالياً") .setContentTitle("Jordan Bot يعمل حالياً")
.setContentText("نحن نراقب الإشعارات ونحدث الخرائط.") .setContentText("نبحث عن طلبات قريبة منك...")
.setSmallIcon(R.drawable.bg_bubble) .setSmallIcon(R.drawable.bg_bubble)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.build() .build()
@@ -69,7 +83,10 @@ class BotForegroundService : Service() {
@SuppressLint("MissingPermission") @SuppressLint("MissingPermission")
private fun startTracking() { 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) .setMinUpdateIntervalMillis(TRACK_INTERVAL_MS)
.build() .build()

View File

@@ -2,70 +2,143 @@ package com.jordanbot.autoride.service
import android.accessibilityservice.AccessibilityService import android.accessibilityservice.AccessibilityService
import android.accessibilityservice.AccessibilityServiceInfo import android.accessibilityservice.AccessibilityServiceInfo
import android.accessibilityservice.GestureDescription
import android.graphics.Path
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import android.view.accessibility.AccessibilityEvent import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo 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() { class RideAccessibilityService : AccessibilityService() {
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val filterEngine = FilterEngine()
override fun onAccessibilityEvent(event: AccessibilityEvent) { override fun onAccessibilityEvent(event: AccessibilityEvent) {
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED && val rootNode = rootInActiveWindow ?: return
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return val packageName = event.packageName?.toString() ?: ""
val source = event.source ?: return // Only process supported apps
val packageName = event.packageName?.toString() ?: return if (!isSupportedApp(packageName)) return
// Check if this package was recently flagged by NotificationListener to be accepted if (event.eventType == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED ||
if (RideNotificationListener.pendingAcceptPackage == packageName) { event.eventType == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) {
val timeSinceRequest = System.currentTimeMillis() - RideNotificationListener.lastRequestTime
// Only accept if within 15 seconds of notification val screenData = scrapeScreen(rootNode, packageName)
if (timeSinceRequest < 15000) { if (screenData != null) {
findAndClickAcceptButton(source) RideDataMerger.updateFromScreen(screenData)
} else {
RideNotificationListener.pendingAcceptPackage = null // Expired
} }
} }
} }
private fun findAndClickAcceptButton(rootNode: AccessibilityNodeInfo) { private fun isSupportedApp(pkg: String): Boolean {
// Arabic and English accept terms return pkg == "com.ubercab.driver" || pkg == "com.careem.adma" || pkg == "me.jeeny.driver"
val acceptTerms = listOf("Accept", "قبول", "Tap to accept", "اضغط للقبول") }
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) { for (term in acceptTerms) {
val nodes = rootNode.findAccessibilityNodeInfosByText(term) val nodes = root.findAccessibilityNodeInfosByText(term)
for (node in nodes) { for (node in nodes) {
if (node.isClickable) { if (node.isClickable || node.parent?.isClickable == true) {
// Simulating human delay (e.g., 500ms) val target = if (node.isClickable) node else node.parent
handler.postDelayed({ Log.d("JordanBot", "🤖 Auto-Accepting via Click: $term")
node.performAction(AccessibilityNodeInfo.ACTION_CLICK) target.performAction(AccessibilityNodeInfo.ACTION_CLICK)
Log.d("JordanBot", "Clicked Accept on node: ${node.text}")
// Reset pending accept after clicking
RideNotificationListener.pendingAcceptPackage = null
}, 500)
return return
} }
} }
} }
if (pkg == "com.ubercab.driver") {
performUberSwipe()
}
} }
override fun onInterrupt() { private fun performUberSwipe() {
Log.e("JordanBot", "Accessibility Service Interrupted") 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() { override fun onServiceConnected() {
super.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 { val info = AccessibilityServiceInfo().apply {
eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED eventTypes = AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED or AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC feedbackType = AccessibilityServiceInfo.FEEDBACK_GENERIC
notificationTimeout = 100 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 this.serviceInfo = info
} }

View File

@@ -70,23 +70,11 @@ class RideNotificationListener : NotificationListenerService() {
Log.d("JordanBot", "Notification Content: $title | $text") Log.d("JordanBot", "Notification Content: $title | $text")
val rideRequest = parser.parse(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) { if (rideRequest != null) {
val isAccepted = filterEngine.evaluate(rideRequest) // Forward to Merger instead of processing here
RideDataMerger.updateFromNotification(rideRequest)
// Send to Backend for Data Mining } else {
sendLogToBackend(rideRequest, isAccepted, "$title - $text") Log.d("JordanBot", "Parser failed to extract ride info from: $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")
}
} }
} }

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

View File

@@ -179,7 +179,7 @@
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="④ تحديث الخرائط (GPS)" android:text="④ الموافقة على الطلبات القريبة"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" /> android:textStyle="bold" />
@@ -188,7 +188,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp" android:layout_marginTop="4dp"
android:text="مطلوب لتتبع مسارك وتحديث خريطة الأردن وحفظ إحصائياتك" android:text="مطلوب لمطابقة الطلبات القريبة من موقعك وتسريع القبول التلقائي"
android:textColor="#999999" android:textColor="#999999"
android:textSize="12sp" /> android:textSize="12sp" />
@@ -198,11 +198,21 @@
android:layout_height="44dp" android:layout_height="44dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:background="@drawable/bg_button" android:background="@drawable/bg_button"
android:text="تفعيل تتبع الموقع" android:text="تفعيل مطابقة الطلبات القريبة"
android:textColor="#FFFFFF" android:textColor="#FFFFFF"
android:textSize="14sp" /> android:textSize="14sp" />
</LinearLayout> </LinearLayout>
<!-- Navigation Buttons -->
<Button
android:id="@+id/btn_subscriptions"
android:layout_width="match_parent"
android:layout_height="48dp"
android:backgroundTint="#2A2A4A"
android:text="💳 إدارة الاشتراكات"
android:textColor="#FFFFFF"
android:layout_marginBottom="24dp" />
<!-- Launch Button --> <!-- Launch Button -->
<Button <Button
android:id="@+id/btn_start" android:id="@+id/btn_start"

View File

@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#1A1A2E"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="💳 الاشتراكات"
android:textColor="#00D4AA"
android:textSize="32sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
android:text="اختر الخطة المناسبة لعملك"
android:textColor="#AAAAAA"
android:textSize="16sp" />
<TextView
android:id="@+id/tv_current_status"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:text="الخطة الحالية: مجاني (1 طلب / يوم)"
android:textAlignment="center"
android:textColor="#FFFFFF"
android:textSize="16sp" />
<!-- Free Plan -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="المجانية"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0 JOD"
android:textColor="#00D4AA"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="• طلب واحد يومياً\n• تجربة الأساسيات"
android:textColor="#AAAAAA"
android:textSize="14sp" />
</LinearLayout>
<!-- Basic Plan -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="الأساسية"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="1 JOD / شهر"
android:textColor="#00D4AA"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="• 10 طلبات يومياً\n• إحصائيات أساسية\n• فلاتر متقدمة"
android:textColor="#AAAAAA"
android:textSize="14sp" />
<Button
android:id="@+id/btn_subscribe_basic"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:backgroundTint="#00D4AA"
android:textColor="#FFFFFF"
android:text="اشترك الآن" />
</LinearLayout>
<!-- Pro Plan -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card"
android:orientation="vertical"
android:padding="16dp"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="الاحترافية"
android:textColor="#FFFFFF"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="2.5 JOD / شهر"
android:textColor="#00D4AA"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="• طلبات غير محدودة\n• أولوية القبول\n• دعم فني مخصص"
android:textColor="#AAAAAA"
android:textSize="14sp" />
<Button
android:id="@+id/btn_subscribe_pro"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:backgroundTint="#FF9800"
android:textColor="#FFFFFF"
android:text="اشترك الآن" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@@ -55,6 +55,51 @@ $latitude = $input['latitude'] ?? null;
$longitude = $input['longitude'] ?? null; $longitude = $input['longitude'] ?? null;
try { try {
// --- Subscription Quota Check ---
if ($isAccepted === 1) {
$today = date('Y-m-d');
// Get active subscription
$stmt = $pdo->prepare("SELECT plan, expires_at FROM subscriptions WHERE fingerprint = :fingerprint AND is_active = 1 ORDER BY id DESC LIMIT 1");
$stmt->execute([':fingerprint' => $fingerprint]);
$sub = $stmt->fetch(PDO::FETCH_ASSOC);
$plan = 'free';
if ($sub) {
$plan = $sub['plan'];
if ($sub['expires_at'] && strtotime($sub['expires_at']) < time()) {
$plan = 'free'; // Expired
}
}
// Get daily usage
$stmt = $pdo->prepare("SELECT rides_accepted FROM daily_usage WHERE fingerprint = :fingerprint AND usage_date = :today");
$stmt->execute([':fingerprint' => $fingerprint, ':today' => $today]);
$usage = $stmt->fetch(PDO::FETCH_ASSOC);
$ridesToday = $usage ? (int)$usage['rides_accepted'] : 0;
// Determine limit
$limit = 1; // free
if ($plan === 'basic') $limit = 10;
if ($plan === 'pro' || $plan === 'annual') $limit = -1;
if ($limit !== -1 && $ridesToday >= $limit) {
http_response_code(403);
echo json_encode([
'success' => false,
'message' => 'Daily limit reached',
'plan' => $plan,
'upgrade_required' => true
]);
exit;
}
// Update daily usage
$stmt = $pdo->prepare("INSERT INTO daily_usage (fingerprint, usage_date, rides_accepted) VALUES (:fingerprint, :today, 1) ON DUPLICATE KEY UPDATE rides_accepted = rides_accepted + 1");
$stmt->execute([':fingerprint' => $fingerprint, ':today' => $today]);
}
// --------------------------------
$sql = "INSERT INTO rides (fingerprint, platform, price, pickup_distance, dropoff_distance, time_to_pickup, pickup_address, dropoff_address, is_accepted, raw_text, latitude, longitude, created_at) $sql = "INSERT INTO rides (fingerprint, platform, price, pickup_distance, dropoff_distance, time_to_pickup, pickup_address, dropoff_address, is_accepted, raw_text, latitude, longitude, created_at)
VALUES (:fingerprint, :platform, :price, :pickup_distance, :dropoff_distance, :time_to_pickup, :pickup_address, :dropoff_address, :is_accepted, :raw_text, :latitude, :longitude, NOW())"; VALUES (:fingerprint, :platform, :price, :pickup_distance, :dropoff_distance, :time_to_pickup, :pickup_address, :dropoff_address, :is_accepted, :raw_text, :latitude, :longitude, NOW())";

View File

@@ -0,0 +1,70 @@
<?php
require_once __DIR__ . '/../../config/db.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method Not Allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$fingerprint = $input['fingerprint'] ?? null;
$plan = $input['plan'] ?? null;
$paymentRef = $input['payment_ref'] ?? null;
if (!$fingerprint || !$plan) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing fingerprint or plan']);
exit;
}
$validPlans = ['free', 'basic', 'pro', 'annual'];
if (!in_array($plan, $validPlans)) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Invalid plan type']);
exit;
}
// Calculate expiration date based on plan
$expiresAt = null;
if ($plan === 'basic' || $plan === 'pro') {
$expiresAt = date('Y-m-d H:i:s', strtotime('+30 days'));
} elseif ($plan === 'annual') {
$expiresAt = date('Y-m-d H:i:s', strtotime('+365 days'));
}
try {
$pdo->beginTransaction();
// Deactivate previous active subscriptions for this device
$stmt = $pdo->prepare("UPDATE subscriptions SET is_active = 0 WHERE fingerprint = :fingerprint");
$stmt->execute([':fingerprint' => $fingerprint]);
// Insert new subscription
$stmt = $pdo->prepare("INSERT INTO subscriptions (fingerprint, plan, expires_at, payment_ref, is_active)
VALUES (:fingerprint, :plan, :expires_at, :payment_ref, 1)");
$stmt->execute([
':fingerprint' => $fingerprint,
':plan' => $plan,
':expires_at' => $expiresAt,
':payment_ref' => $paymentRef
]);
$pdo->commit();
echo json_encode([
'success' => true,
'message' => 'Subscription activated successfully',
'plan' => $plan,
'expires_at' => $expiresAt
]);
} catch (PDOException $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}

View File

@@ -0,0 +1,68 @@
<?php
require_once __DIR__ . '/../../config/db.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['success' => false, 'message' => 'Method Not Allowed']);
exit;
}
$input = json_decode(file_get_contents('php://input'), true);
$fingerprint = $input['fingerprint'] ?? null;
if (!$fingerprint) {
http_response_code(400);
echo json_encode(['success' => false, 'message' => 'Missing fingerprint']);
exit;
}
try {
// 1. Get Subscription Status
$stmt = $pdo->prepare("SELECT * FROM subscriptions WHERE fingerprint = :fingerprint AND is_active = 1 ORDER BY id DESC LIMIT 1");
$stmt->execute([':fingerprint' => $fingerprint]);
$sub = $stmt->fetch(PDO::FETCH_ASSOC);
$plan = $sub ? $sub['plan'] : 'free';
$expiresAt = $sub ? $sub['expires_at'] : null;
// Check expiration
if ($expiresAt && strtotime($expiresAt) < time()) {
// Expired, revert to free
$stmt = $pdo->prepare("UPDATE subscriptions SET is_active = 0 WHERE id = :id");
$stmt->execute([':id' => $sub['id']]);
$plan = 'free';
$expiresAt = null;
}
// 2. Get Daily Usage
$today = date('Y-m-d');
$stmt = $pdo->prepare("SELECT rides_accepted FROM daily_usage WHERE fingerprint = :fingerprint AND usage_date = :today");
$stmt->execute([
':fingerprint' => $fingerprint,
':today' => $today
]);
$usage = $stmt->fetch(PDO::FETCH_ASSOC);
$ridesToday = $usage ? (int)$usage['rides_accepted'] : 0;
// 3. Determine limits
$limit = 1; // Default for free
if ($plan === 'basic') $limit = 10;
if ($plan === 'pro' || $plan === 'annual') $limit = -1; // Unlimited
$canAccept = ($limit === -1) || ($ridesToday < $limit);
echo json_encode([
'success' => true,
'plan' => $plan,
'expires_at' => $expiresAt,
'rides_today' => $ridesToday,
'rides_limit' => $limit,
'can_accept' => $canAccept
]);
} catch (PDOException $e) {
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Database error: ' . $e->getMessage()]);
}

View File

@@ -23,6 +23,29 @@ CREATE TABLE IF NOT EXISTS rides (
INDEX (platform) INDEX (platform)
) ENGINE=InnoDB; ) ENGINE=InnoDB;
-- Subscription System
CREATE TABLE IF NOT EXISTS subscriptions (
id INT AUTO_INCREMENT PRIMARY KEY,
fingerprint VARCHAR(255) NOT NULL,
plan ENUM('free', 'basic', 'pro', 'annual') DEFAULT 'free',
started_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP NULL,
is_active TINYINT(1) DEFAULT 1,
payment_ref VARCHAR(255),
INDEX (fingerprint),
INDEX (expires_at)
) ENGINE=InnoDB;
-- Daily Usage Quotas
CREATE TABLE IF NOT EXISTS daily_usage (
id INT AUTO_INCREMENT PRIMARY KEY,
fingerprint VARCHAR(255) NOT NULL,
usage_date DATE NOT NULL,
rides_accepted INT DEFAULT 0,
UNIQUE KEY unique_daily (fingerprint, usage_date),
INDEX (fingerprint)
) ENGINE=InnoDB;
-- Table to store driver locations for map updating -- Table to store driver locations for map updating
CREATE TABLE IF NOT EXISTS driver_locations ( CREATE TABLE IF NOT EXISTS driver_locations (
id BIGINT AUTO_INCREMENT PRIMARY KEY, id BIGINT AUTO_INCREMENT PRIMARY KEY,