Update: 2026-05-15 03:09:36
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<EFBFBD>h
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,2 @@
|
||||
30
|
||||
40
|
||||
0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1 +1 @@
|
||||
Α·Ο‡ύΊΛΊ<EFBFBD><EFBFBD>ΰ¶
|
||||
ֱ·ֿ‡÷ֻ÷ׂ<EFBFBD>א¶ת₪
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -43,6 +43,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity android:name=".SubscriptionActivity" android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".service.RideNotificationListener"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -25,12 +26,25 @@ class BotForegroundService : Service() {
|
||||
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()
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -179,7 +179,7 @@
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="④ تحديث الخرائط (GPS)"
|
||||
android:text="④ الموافقة على الطلبات القريبة"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
@@ -188,7 +188,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="مطلوب لتتبع مسارك وتحديث خريطة الأردن وحفظ إحصائياتك"
|
||||
android:text="مطلوب لمطابقة الطلبات القريبة من موقعك وتسريع القبول التلقائي"
|
||||
android:textColor="#999999"
|
||||
android:textSize="12sp" />
|
||||
|
||||
@@ -198,11 +198,21 @@
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_button"
|
||||
android:text="تفعيل تتبع الموقع"
|
||||
android:text="تفعيل مطابقة الطلبات القريبة"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp" />
|
||||
</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 -->
|
||||
<Button
|
||||
android:id="@+id/btn_start"
|
||||
|
||||
163
app/src/main/res/layout/activity_subscription.xml
Normal file
163
app/src/main/res/layout/activity_subscription.xml
Normal 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>
|
||||
@@ -55,6 +55,51 @@ $latitude = $input['latitude'] ?? null;
|
||||
$longitude = $input['longitude'] ?? null;
|
||||
|
||||
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)
|
||||
VALUES (:fingerprint, :platform, :price, :pickup_distance, :dropoff_distance, :time_to_pickup, :pickup_address, :dropoff_address, :is_accepted, :raw_text, :latitude, :longitude, NOW())";
|
||||
|
||||
|
||||
70
backend/api/subscription/activate.php
Normal file
70
backend/api/subscription/activate.php
Normal 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()]);
|
||||
}
|
||||
68
backend/api/subscription/check.php
Normal file
68
backend/api/subscription/check.php
Normal 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()]);
|
||||
}
|
||||
@@ -23,6 +23,29 @@ CREATE TABLE IF NOT EXISTS rides (
|
||||
INDEX (platform)
|
||||
) 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
|
||||
CREATE TABLE IF NOT EXISTS driver_locations (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
|
||||
Reference in New Issue
Block a user