first
This commit is contained in:
BIN
app/src/main/java/com/jordanbot/.DS_Store
vendored
Normal file
BIN
app/src/main/java/com/jordanbot/.DS_Store
vendored
Normal file
Binary file not shown.
118
app/src/main/java/com/jordanbot/autoride/MainActivity.kt
Normal file
118
app/src/main/java/com/jordanbot/autoride/MainActivity.kt
Normal file
@@ -0,0 +1,118 @@
|
||||
package com.jordanbot.autoride
|
||||
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityManager
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import com.jordanbot.autoride.service.BotForegroundService
|
||||
import com.jordanbot.autoride.service.OverlayService
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var tvStatus: TextView
|
||||
private lateinit var btnStart: Button
|
||||
private lateinit var btnStop: Button
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
tvStatus = findViewById(R.id.tv_status)
|
||||
btnStart = findViewById(R.id.btn_start)
|
||||
btnStop = findViewById(R.id.btn_stop)
|
||||
|
||||
findViewById<Button>(R.id.btn_notification).setOnClickListener {
|
||||
startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btn_accessibility).setOnClickListener {
|
||||
startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS))
|
||||
}
|
||||
|
||||
findViewById<Button>(R.id.btn_overlay).setOnClickListener {
|
||||
if (!Settings.canDrawOverlays(this)) {
|
||||
startActivity(Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION))
|
||||
} else {
|
||||
Toast.makeText(this, "هذه الصلاحية مفعلة بالفعل", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
btnStart.setOnClickListener {
|
||||
if (checkAllPermissions()) {
|
||||
startBot()
|
||||
} else {
|
||||
Toast.makeText(this, "الرجاء تفعيل جميع الصلاحيات أولاً", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
btnStop.setOnClickListener {
|
||||
stopBot()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
updateStatusUI()
|
||||
}
|
||||
|
||||
private fun checkAllPermissions(): Boolean {
|
||||
val notificationEnabled = NotificationManagerCompat.getEnabledListenerPackages(this).contains(packageName)
|
||||
val overlayEnabled = Settings.canDrawOverlays(this)
|
||||
|
||||
// Check Accessibility
|
||||
var accessibilityEnabled = false
|
||||
val am = getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
||||
val enabledServices = am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK)
|
||||
for (service in enabledServices) {
|
||||
if (service.id.contains(packageName)) {
|
||||
accessibilityEnabled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return notificationEnabled && overlayEnabled && accessibilityEnabled
|
||||
}
|
||||
|
||||
private fun updateStatusUI() {
|
||||
if (checkAllPermissions()) {
|
||||
tvStatus.text = "✅ جاهز للتشغيل"
|
||||
tvStatus.setTextColor(Color.parseColor("#00D4AA"))
|
||||
} else {
|
||||
tvStatus.text = "⚠️ بانتظار الصلاحيات"
|
||||
tvStatus.setTextColor(Color.parseColor("#FFCA28"))
|
||||
}
|
||||
}
|
||||
|
||||
private fun startBot() {
|
||||
startService(Intent(this, OverlayService::class.java))
|
||||
startService(Intent(this, BotForegroundService::class.java))
|
||||
|
||||
tvStatus.text = "⚡ البوت يعمل حالياً"
|
||||
tvStatus.setTextColor(Color.parseColor("#00D4AA"))
|
||||
|
||||
btnStart.visibility = View.GONE
|
||||
btnStop.visibility = View.VISIBLE
|
||||
|
||||
Toast.makeText(this, "تم التشغيل بنجاح", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun stopBot() {
|
||||
stopService(Intent(this, OverlayService::class.java))
|
||||
stopService(Intent(this, BotForegroundService::class.java))
|
||||
|
||||
tvStatus.text = "⏸ البوت متوقف"
|
||||
tvStatus.setTextColor(Color.parseColor("#FF5252"))
|
||||
|
||||
btnStart.visibility = View.VISIBLE
|
||||
btnStop.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
56
app/src/main/java/com/jordanbot/autoride/api/BackendApi.kt
Normal file
56
app/src/main/java/com/jordanbot/autoride/api/BackendApi.kt
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.jordanbot.autoride.api
|
||||
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonGsonConverterFactory
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
// Data models for the API
|
||||
data class RideLogRequest(
|
||||
val platform: String,
|
||||
val price: Double,
|
||||
val pickupDistance: String,
|
||||
val dropoffDistance: String,
|
||||
val timeToPickup: String,
|
||||
val isAccepted: Boolean,
|
||||
val rawText: String,
|
||||
val fingerprint: String
|
||||
)
|
||||
|
||||
data class LocationPoint(
|
||||
val latitude: Double,
|
||||
val longitude: Double,
|
||||
val speed: Float,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
data class BulkLocationRequest(
|
||||
val fingerprint: String,
|
||||
val locations: List<LocationPoint>
|
||||
)
|
||||
|
||||
data class ApiResponse(
|
||||
val success: Boolean,
|
||||
val message: 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
|
||||
}
|
||||
|
||||
object ApiClient {
|
||||
// Replace with your actual server URL
|
||||
private const val BASE_URL = "https://lawer.tripz-egypt.com/jordan_bot/"
|
||||
|
||||
val service: BackendApiService by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(BASE_URL)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
.create(BackendApiService::class.java)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jordanbot.autoride.filter
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
class FilterEngine {
|
||||
|
||||
// These will eventually come from SharedPreferences / UI
|
||||
var minPriceJod: Double = 1.0
|
||||
var maxMinutesAway: Int = 10
|
||||
var isEnabled: Boolean = true
|
||||
|
||||
fun evaluate(request: RideRequest): Boolean {
|
||||
if (!isEnabled) return false
|
||||
|
||||
// Check Price
|
||||
if (request.priceJod != null && request.priceJod < minPriceJod) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Distance/Time
|
||||
if (request.minutesAway != null && request.minutesAway > maxMinutesAway) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Passes all filters
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.jordanbot.autoride.model
|
||||
|
||||
data class RideRequest(
|
||||
val appPackage: String,
|
||||
val priceJod: Double?,
|
||||
val minutesAway: Int?,
|
||||
val distanceKm: Double?,
|
||||
val title: String,
|
||||
val text: String
|
||||
)
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.jordanbot.autoride.parser
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
class CareemParser : NotificationParser {
|
||||
override val packageName = "com.careem.adma"
|
||||
|
||||
override fun parse(title: String, text: String): RideRequest? {
|
||||
var price: Double? = null
|
||||
var minutes: Int? = null
|
||||
|
||||
// Careem format examples: "رحلة جديدة - 2.5 د.أ", "New ride - 2.5 JOD"
|
||||
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ|دينار)""".toRegex()
|
||||
val minutesRegex = """(\d+)\s*(min|دقيقة|دقائق)""".toRegex()
|
||||
|
||||
val fullText = "$title $text"
|
||||
|
||||
priceRegex.find(fullText)?.let {
|
||||
price = it.groupValues[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
minutesRegex.find(fullText)?.let {
|
||||
minutes = it.groupValues[1].toIntOrNull()
|
||||
}
|
||||
|
||||
return RideRequest(
|
||||
appPackage = packageName,
|
||||
priceJod = price,
|
||||
minutesAway = minutes,
|
||||
distanceKm = null,
|
||||
title = title,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.jordanbot.autoride.parser
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
class JeenyParser : NotificationParser {
|
||||
override val packageName = "me.com.easytaxista"
|
||||
|
||||
override fun parse(title: String, text: String): RideRequest? {
|
||||
var price: Double? = null
|
||||
var distance: Double? = null
|
||||
|
||||
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ)""".toRegex()
|
||||
// Jeeny often shows distance in km instead of minutes
|
||||
val distanceRegex = """(\d+\.?\d*)\s*(km|كم)""".toRegex()
|
||||
|
||||
val fullText = "$title $text"
|
||||
|
||||
priceRegex.find(fullText)?.let {
|
||||
price = it.groupValues[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
distanceRegex.find(fullText)?.let {
|
||||
distance = it.groupValues[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
return RideRequest(
|
||||
appPackage = packageName,
|
||||
priceJod = price,
|
||||
minutesAway = null,
|
||||
distanceKm = distance,
|
||||
title = title,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.jordanbot.autoride.parser
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
interface NotificationParser {
|
||||
val packageName: String
|
||||
fun parse(title: String, text: String): RideRequest?
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.jordanbot.autoride.parser
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
class PetraRideParser : NotificationParser {
|
||||
// Note: Package name might vary (e.g. com.PetraRide_Captain), updating to generic if needed
|
||||
override val packageName = "com.petraride.captain"
|
||||
|
||||
override fun parse(title: String, text: String): RideRequest? {
|
||||
var price: Double? = null
|
||||
|
||||
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ)""".toRegex()
|
||||
|
||||
val fullText = "$title $text"
|
||||
|
||||
priceRegex.find(fullText)?.let {
|
||||
price = it.groupValues[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
return RideRequest(
|
||||
appPackage = packageName,
|
||||
priceJod = price,
|
||||
minutesAway = null,
|
||||
distanceKm = null,
|
||||
title = title,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.jordanbot.autoride.parser
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
class TaxiFParser : NotificationParser {
|
||||
override val packageName = "com.taxif.driver"
|
||||
|
||||
override fun parse(title: String, text: String): RideRequest? {
|
||||
var price: Double? = null
|
||||
|
||||
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ)""".toRegex()
|
||||
|
||||
val fullText = "$title $text"
|
||||
|
||||
priceRegex.find(fullText)?.let {
|
||||
price = it.groupValues[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
return RideRequest(
|
||||
appPackage = packageName,
|
||||
priceJod = price,
|
||||
minutesAway = null,
|
||||
distanceKm = null,
|
||||
title = title,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.jordanbot.autoride.parser
|
||||
|
||||
import com.jordanbot.autoride.model.RideRequest
|
||||
|
||||
class UberParser : NotificationParser {
|
||||
override val packageName = "com.ubercab.driver"
|
||||
|
||||
override fun parse(title: String, text: String): RideRequest? {
|
||||
// Example: "New trip request • 2.50 JOD • 5 min away"
|
||||
// This is a basic regex, needs adjustment based on actual Uber notification in Jordan
|
||||
|
||||
var price: Double? = null
|
||||
var minutes: Int? = null
|
||||
|
||||
val priceRegex = """(\d+\.?\d*)\s*(JOD|د\.أ)""".toRegex()
|
||||
val minutesRegex = """(\d+)\s*(min|دقيقة)""".toRegex()
|
||||
|
||||
val fullText = "$title $text"
|
||||
|
||||
priceRegex.find(fullText)?.let {
|
||||
price = it.groupValues[1].toDoubleOrNull()
|
||||
}
|
||||
|
||||
minutesRegex.find(fullText)?.let {
|
||||
minutes = it.groupValues[1].toIntOrNull()
|
||||
}
|
||||
|
||||
// We return the request even if price is null, we can filter it later
|
||||
return RideRequest(
|
||||
appPackage = packageName,
|
||||
priceJod = price,
|
||||
minutesAway = minutes,
|
||||
distanceKm = null, // Uber usually gives mins, not always distance
|
||||
title = title,
|
||||
text = text
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.jordanbot.autoride.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.google.android.gms.location.*
|
||||
import com.jordanbot.autoride.R
|
||||
import com.jordanbot.autoride.api.ApiClient
|
||||
import com.jordanbot.autoride.api.BulkLocationRequest
|
||||
import com.jordanbot.autoride.api.LocationPoint
|
||||
import com.jordanbot.autoride.utils.DeviceUtils
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
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.MINUTES.toMillis(5)
|
||||
private val TRACK_INTERVAL_MS = TimeUnit.SECONDS.toMillis(3)
|
||||
|
||||
private val locationCallback = object : LocationCallback() {
|
||||
override fun onLocationResult(locationResult: LocationResult) {
|
||||
locationResult.lastLocation?.let { location ->
|
||||
val point = LocationPoint(
|
||||
latitude = location.latitude,
|
||||
longitude = location.longitude,
|
||||
speed = location.speed,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
synchronized(locationBuffer) {
|
||||
locationBuffer.add(point)
|
||||
}
|
||||
Log.d("JordanBot", "📍 Location tracked: ${location.latitude}, ${location.longitude}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
|
||||
startTracking()
|
||||
startUploadTimer()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Jordan Bot يعمل حالياً")
|
||||
.setContentText("نحن نراقب الإشعارات ونحدث الخرائط.")
|
||||
.setSmallIcon(R.drawable.bg_bubble)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
|
||||
startForeground(1, notification)
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun startTracking() {
|
||||
val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, TRACK_INTERVAL_MS)
|
||||
.setMinUpdateIntervalMillis(TRACK_INTERVAL_MS)
|
||||
.build()
|
||||
|
||||
fusedLocationClient.requestLocationUpdates(
|
||||
locationRequest,
|
||||
locationCallback,
|
||||
Looper.getMainLooper()
|
||||
)
|
||||
}
|
||||
|
||||
private fun startUploadTimer() {
|
||||
serviceScope.launch {
|
||||
while (isActive) {
|
||||
delay(UPLOAD_INTERVAL_MS)
|
||||
uploadLocations()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun uploadLocations() {
|
||||
val pointsToUpload = synchronized(locationBuffer) {
|
||||
val copy = locationBuffer.toList()
|
||||
locationBuffer.clear()
|
||||
copy
|
||||
}
|
||||
|
||||
if (pointsToUpload.isEmpty()) return
|
||||
|
||||
val fingerprint = DeviceUtils.getDeviceFingerprint(this)
|
||||
val request = BulkLocationRequest(fingerprint, pointsToUpload)
|
||||
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val response = ApiClient.service.updateBulkLocation(request)
|
||||
if (response.success) {
|
||||
Log.d("JordanBot", "✅ Successfully uploaded ${pointsToUpload.size} locations")
|
||||
} else {
|
||||
Log.e("JordanBot", "❌ Failed to upload locations: ${response.message}")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("JordanBot", "❌ Error uploading locations", e)
|
||||
// If failed, we might want to add them back to the buffer,
|
||||
// but for now we just log to keep it simple.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val serviceChannel = NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
"Jordan Bot Service Channel",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
val manager = getSystemService(NotificationManager::class.java)
|
||||
manager.createNotificationChannel(serviceChannel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback)
|
||||
serviceScope.cancel()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val CHANNEL_ID = "JordanBotForegroundChannel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package com.jordanbot.autoride.service
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.PixelFormat
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.Button
|
||||
import android.widget.EditText
|
||||
import android.widget.Switch
|
||||
import android.widget.TextView
|
||||
import com.jordanbot.autoride.R
|
||||
|
||||
class OverlayService : Service() {
|
||||
|
||||
private lateinit var windowManager: WindowManager
|
||||
private lateinit var floatingView: View
|
||||
private lateinit var expandedView: View
|
||||
private lateinit var params: WindowManager.LayoutParams
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
|
||||
floatingView = inflater.inflate(R.layout.overlay_floating, null)
|
||||
expandedView = inflater.inflate(R.layout.overlay_expanded, null)
|
||||
|
||||
val layoutFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
} else {
|
||||
WindowManager.LayoutParams.TYPE_PHONE
|
||||
}
|
||||
|
||||
params = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
layoutFlag,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
|
||||
params.gravity = Gravity.TOP or Gravity.START
|
||||
params.x = 0
|
||||
params.y = 100
|
||||
|
||||
windowManager.addView(floatingView, params)
|
||||
|
||||
setupFloatingView()
|
||||
setupExpandedView()
|
||||
}
|
||||
|
||||
private fun setupFloatingView() {
|
||||
val tvBubble = floatingView.findViewById<TextView>(R.id.tv_bubble)
|
||||
|
||||
floatingView.setOnTouchListener(object : View.OnTouchListener {
|
||||
private var initialX: Int = 0
|
||||
private var initialY: Int = 0
|
||||
private var initialTouchX: Float = 0f
|
||||
private var initialTouchY: Float = 0f
|
||||
private var isClick = false
|
||||
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
initialX = params.x
|
||||
initialY = params.y
|
||||
initialTouchX = event.rawX
|
||||
initialTouchY = event.rawY
|
||||
isClick = true
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val diffX = (event.rawX - initialTouchX).toInt()
|
||||
val diffY = (event.rawY - initialTouchY).toInt()
|
||||
|
||||
if (Math.abs(diffX) > 10 || Math.abs(diffY) > 10) {
|
||||
isClick = false
|
||||
}
|
||||
|
||||
params.x = initialX + diffX
|
||||
params.y = initialY + diffY
|
||||
windowManager.updateViewLayout(floatingView, params)
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (isClick) {
|
||||
showExpandedView()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupExpandedView() {
|
||||
val btnClose = expandedView.findViewById<Button>(R.id.btn_save_close)
|
||||
val etMinPrice = expandedView.findViewById<EditText>(R.id.et_min_price)
|
||||
val switchStatus = expandedView.findViewById<Switch>(R.id.switch_bot_status)
|
||||
|
||||
btnClose.setOnClickListener {
|
||||
// TODO: Save to SharedPreferences/FilterEngine
|
||||
val minPriceStr = etMinPrice.text.toString()
|
||||
val isEnabled = switchStatus.isChecked
|
||||
|
||||
// Update bubble UI
|
||||
val tvBubble = floatingView.findViewById<TextView>(R.id.tv_bubble)
|
||||
if (isEnabled) {
|
||||
tvBubble.text = "Bot\nON"
|
||||
tvBubble.setBackgroundResource(R.drawable.bg_bubble)
|
||||
} else {
|
||||
tvBubble.text = "Bot\nOFF"
|
||||
// Would need a red bubble background here in a real app
|
||||
}
|
||||
|
||||
hideExpandedView()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showExpandedView() {
|
||||
windowManager.removeView(floatingView)
|
||||
|
||||
// Change params to focusable so user can type in EditText
|
||||
val expandedParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
|
||||
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
expandedParams.gravity = Gravity.CENTER
|
||||
|
||||
windowManager.addView(expandedView, expandedParams)
|
||||
}
|
||||
|
||||
private fun hideExpandedView() {
|
||||
windowManager.removeView(expandedView)
|
||||
|
||||
// Reset params to non-focusable
|
||||
params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
|
||||
windowManager.addView(floatingView, params)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (::floatingView.isInitialized) windowManager.removeView(floatingView)
|
||||
if (::expandedView.isInitialized && expandedView.parent != null) windowManager.removeView(expandedView)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.jordanbot.autoride.service
|
||||
|
||||
import android.accessibilityservice.AccessibilityService
|
||||
import android.accessibilityservice.AccessibilityServiceInfo
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
class RideAccessibilityService : AccessibilityService() {
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onAccessibilityEvent(event: AccessibilityEvent) {
|
||||
if (event.eventType != AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED &&
|
||||
event.eventType != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED) return
|
||||
|
||||
val source = event.source ?: return
|
||||
val packageName = event.packageName?.toString() ?: return
|
||||
|
||||
// Check if this package was recently flagged by NotificationListener to be accepted
|
||||
if (RideNotificationListener.pendingAcceptPackage == packageName) {
|
||||
val timeSinceRequest = System.currentTimeMillis() - RideNotificationListener.lastRequestTime
|
||||
|
||||
// Only accept if within 15 seconds of notification
|
||||
if (timeSinceRequest < 15000) {
|
||||
findAndClickAcceptButton(source)
|
||||
} else {
|
||||
RideNotificationListener.pendingAcceptPackage = null // Expired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun findAndClickAcceptButton(rootNode: AccessibilityNodeInfo) {
|
||||
// Arabic and English accept terms
|
||||
val acceptTerms = listOf("Accept", "قبول", "Tap to accept", "اضغط للقبول")
|
||||
|
||||
for (term in acceptTerms) {
|
||||
val nodes = rootNode.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)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onInterrupt() {
|
||||
Log.e("JordanBot", "Accessibility Service Interrupted")
|
||||
}
|
||||
|
||||
override fun onServiceConnected() {
|
||||
super.onServiceConnected()
|
||||
Log.d("JordanBot", "Accessibility Service Connected")
|
||||
|
||||
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
|
||||
}
|
||||
this.serviceInfo = info
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.jordanbot.autoride.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.service.notification.NotificationListenerService
|
||||
import android.service.notification.StatusBarNotification
|
||||
import android.util.Log
|
||||
import com.jordanbot.autoride.api.ApiClient
|
||||
import com.jordanbot.autoride.api.RideLogRequest
|
||||
import com.jordanbot.autoride.filter.FilterEngine
|
||||
import com.jordanbot.autoride.parser.*
|
||||
import com.jordanbot.autoride.utils.DeviceUtils
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RideNotificationListener : NotificationListenerService() {
|
||||
|
||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
private val filterEngine = FilterEngine()
|
||||
private val parsers = listOf(
|
||||
UberParser(),
|
||||
CareemParser(),
|
||||
JeenyParser(),
|
||||
PetraRideParser(),
|
||||
TaxiFParser()
|
||||
)
|
||||
|
||||
companion object {
|
||||
var pendingAcceptPackage: String? = null
|
||||
var lastRequestTime: Long = 0
|
||||
}
|
||||
|
||||
override fun onNotificationPosted(sbn: StatusBarNotification) {
|
||||
val packageName = sbn.packageName
|
||||
val parser = parsers.find { it.packageName == packageName } ?: return
|
||||
|
||||
val notification = sbn.notification
|
||||
val extras = notification.extras
|
||||
val title = extras.getString(Notification.EXTRA_TITLE) ?: ""
|
||||
val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: ""
|
||||
|
||||
Log.d("JordanBot", "Notification from $packageName: $title - $text")
|
||||
|
||||
val rideRequest = parser.parse(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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendLogToBackend(ride: com.jordanbot.autoride.model.RideRequest, isAccepted: Boolean, rawText: String) {
|
||||
val logRequest = RideLogRequest(
|
||||
platform = ride.platform,
|
||||
price = ride.price,
|
||||
pickupDistance = ride.pickupDistance,
|
||||
dropoffDistance = ride.dropoffDistance,
|
||||
timeToPickup = ride.timeToPickup,
|
||||
isAccepted = isAccepted,
|
||||
rawText = rawText,
|
||||
fingerprint = DeviceUtils.getDeviceFingerprint(this)
|
||||
)
|
||||
|
||||
serviceScope.launch {
|
||||
try {
|
||||
val response = ApiClient.service.logRide(logRequest)
|
||||
if (response.success) {
|
||||
Log.d("JordanBot", "✅ Ride log sent to server")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("JordanBot", "❌ Failed to send ride log", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onListenerConnected() {
|
||||
super.onListenerConnected()
|
||||
Log.d("JordanBot", "Notification Listener Connected")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.jordanbot.autoride.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.provider.Settings
|
||||
|
||||
object DeviceUtils {
|
||||
/**
|
||||
* Returns a unique fingerprint for the device based on Android ID.
|
||||
*/
|
||||
fun getDeviceFingerprint(context: Context): String {
|
||||
return Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID) ?: "UNKNOWN_DEVICE"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user