first commit

This commit is contained in:
Hamza-Ayed
2026-06-09 08:40:31 +03:00
commit d8901e1a87
3161 changed files with 536187 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.siro.trip_overlay_plugin">
</manifest>

View File

@@ -0,0 +1,179 @@
package com.intaleq_driver.trip_overlay_plugin
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
class TripOverlayPlugin :
FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
private lateinit var channel: MethodChannel
private lateinit var context: Context
private var activity: Activity? = null
companion object {
const val CHANNEL = "trip_overlay_plugin"
const val TAG = "TripOverlayPlugin"
const val REQUEST_CODE_OVERLAY = 1001
// Static reference so TripOverlayService can call back into Flutter
var methodChannel: MethodChannel? = null
/**
* Called by TripOverlayService when the driver taps Accept. Sends event Flutter → Dart
* side.
*/
fun notifyTripAccepted(tripId: String) {
methodChannel?.invokeMethod("onTripAccepted", mapOf("tripId" to tripId))
Log.d(TAG, "notifyTripAccepted: $tripId")
}
/** Called by TripOverlayService when the driver taps Reject or timer expires. */
fun notifyTripRejected(tripId: String) {
methodChannel?.invokeMethod("onTripRejected", mapOf("tripId" to tripId))
Log.d(TAG, "notifyTripRejected: $tripId")
}
}
// ─── FlutterPlugin ───────────────────────────────────────────────────────
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
channel = MethodChannel(binding.binaryMessenger, CHANNEL)
channel.setMethodCallHandler(this)
methodChannel = channel
Log.d(TAG, "Plugin attached to engine")
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
methodChannel = null
}
// ─── MethodCallHandler ───────────────────────────────────────────────────
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"isPermissionGranted" -> {
result.success(isOverlayPermissionGranted())
}
"requestPermission" -> {
requestOverlayPermission()
result.success(null)
}
"showOverlay" -> {
val tripDataJson =
call.argument<String>("tripData")
?: run {
result.error("INVALID_ARGS", "tripData is required", null)
return
}
val autoCloseSeconds = call.argument<Int>("autoCloseSeconds") ?: 30
val success = showOverlay(tripDataJson, autoCloseSeconds)
result.success(success)
}
"hideOverlay" -> {
hideOverlay()
result.success(null)
}
"isOverlayActive" -> {
result.success(TripOverlayService.isRunning)
}
else -> result.notImplemented()
}
}
// ─── Permission Helpers ───────────────────────────────────────────────────
private fun isOverlayPermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
Settings.canDrawOverlays(context)
} else {
true // Pre-M devices don't need runtime permission
}
}
private fun requestOverlayPermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val intent =
Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:${context.packageName}")
)
.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
activity?.startActivityForResult(intent, REQUEST_CODE_OVERLAY)
?: context.startActivity(intent)
}
}
// ─── Overlay Control ──────────────────────────────────────────────────────
private fun showOverlay(tripDataJson: String, autoCloseSeconds: Int): Boolean {
if (!isOverlayPermissionGranted()) {
Log.w(TAG, "Overlay permission not granted")
return false
}
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_SHOW
putExtra(TripOverlayService.EXTRA_TRIP_DATA, tripDataJson)
putExtra(TripOverlayService.EXTRA_AUTO_CLOSE_SECONDS, autoCloseSeconds)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Log.d(TAG, "showOverlay called with tripData: $tripDataJson")
return true
}
private fun hideOverlay() {
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_HIDE
}
context.startService(intent)
Log.d(TAG, "hideOverlay called")
}
// ─── ActivityAware ────────────────────────────────────────────────────────
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
}
// ─── ActivityResultListener ───────────────────────────────────────────────
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == REQUEST_CODE_OVERLAY) {
Log.d(TAG, "Overlay permission result received")
return true
}
return false
}
}

View File

@@ -0,0 +1,109 @@
package com.intaleq_driver.trip_overlay_plugin
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.util.Log
import org.json.JSONObject
/**
* TripOverlayReceiver
*
* A BroadcastReceiver you can trigger directly from your FCM background handler (or anywhere
* outside Flutter context) to show the overlay without needing an active MethodChannel.
*
* Usage from your backgroundMessageHandler in main.dart: — Call the static helper
* [TripOverlayReceiver.show(context, tripDataJson)]
*
* Or register in AndroidManifest and send an explicit broadcast.
*
* Register in AndroidManifest.xml: <receiver
* ```
* android:name="com.trip_overlay.TripOverlayReceiver"
* android:exported="false" />
* ```
*/
class TripOverlayReceiver : BroadcastReceiver() {
companion object {
const val TAG = "TripOverlayReceiver"
const val ACTION = "com.intaleq_driver.SHOW_OVERLAY"
const val EXTRA_TRIP_DATA = "trip_data"
const val EXTRA_AUTO_CLOSE = "auto_close_seconds"
/**
* Convenience method — call this from your FCM handler (Kotlin/Java) or from a Flutter
* MethodChannel invocation.
*/
fun show(context: Context, tripDataJson: String, autoCloseSeconds: Int = 30) {
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_SHOW
putExtra(TripOverlayService.EXTRA_TRIP_DATA, tripDataJson)
putExtra(TripOverlayService.EXTRA_AUTO_CLOSE_SECONDS, autoCloseSeconds)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Log.d(TAG, "Dispatched show overlay request")
}
/** Hide the overlay programmatically from native side */
fun hide(context: Context) {
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_HIDE
}
context.startService(intent)
Log.d(TAG, "Dispatched hide overlay request")
}
/**
* Build a TripData JSON string from FCM message data map. Keys match what your server sends
* inside the FCM data payload.
*
* Expected FCM data keys: tripId, passengerName, pickupAddress, dropoffAddress, distanceKm,
* estimatedFare, estimatedMinutes, pickupLat, pickupLng
*/
fun buildTripJsonFromFcmData(data: Map<String, String>): String {
return try {
JSONObject()
.apply {
put("tripId", data["tripId"] ?: "")
put("passengerName", data["passengerName"] ?: "غير معروف")
put("pickupAddress", data["pickupAddress"] ?: "")
put("dropoffAddress", data["dropoffAddress"] ?: "")
put("distanceKm", data["distanceKm"]?.toDoubleOrNull() ?: 0.0)
put("estimatedFare", data["estimatedFare"]?.toDoubleOrNull() ?: 0.0)
put("estimatedMinutes", data["estimatedMinutes"]?.toIntOrNull() ?: 0)
put("pickupLat", data["pickupLat"]?.toDoubleOrNull() ?: 0.0)
put("pickupLng", data["pickupLng"]?.toDoubleOrNull() ?: 0.0)
put("passengerAvatarUrl", data["passengerAvatarUrl"] ?: "")
}
.toString()
} catch (e: Exception) {
Log.e(TAG, "Error building trip JSON: ${e.message}")
"{}"
}
}
}
// ─── Broadcast received ───────────────────────────────────────────────────
override fun onReceive(context: Context, intent: Intent) {
if (intent.action != ACTION) return
val tripData =
intent.getStringExtra(EXTRA_TRIP_DATA)
?: run {
Log.e(TAG, "No trip data in broadcast intent")
return
}
val autoClose = intent.getIntExtra(EXTRA_AUTO_CLOSE, 30)
show(context, tripData, autoClose)
Log.d(TAG, "Received broadcast, showing overlay")
}
}

View File

@@ -0,0 +1,560 @@
package com.intaleq_driver.trip_overlay_plugin
import android.app.*
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color
import android.graphics.PixelFormat
import android.graphics.drawable.GradientDrawable
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.os.Build
import android.os.CountDownTimer
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.view.*
import android.widget.*
import androidx.core.app.NotificationCompat
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
import org.json.JSONObject
class TripOverlayService : Service() {
companion object {
const val TAG = "TripOverlayService"
const val ACTION_SHOW = "SHOW"
const val ACTION_HIDE = "HIDE"
const val EXTRA_TRIP_DATA = "trip_data"
const val EXTRA_AUTO_CLOSE_SECONDS = "auto_close_seconds"
private const val NOTIFICATION_ID = 9900
private const val CHANNEL_ID = "trip_overlay_service_channel"
@Volatile
var isRunning: Boolean = false
private set
}
private var windowManager: WindowManager? = null
private var overlayView: View? = null
private var countDownTimer: CountDownTimer? = null
private var currentTripId: String = ""
private var mediaPlayer: MediaPlayer? = null // 🔴 مشغل الصوت
override fun onBind(intent: Intent?): IBinder? = null
override fun onCreate() {
super.onCreate()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_SHOW -> {
val tripDataJson = intent.getStringExtra(EXTRA_TRIP_DATA) ?: return START_NOT_STICKY
val autoClose = intent.getIntExtra(EXTRA_AUTO_CLOSE_SECONDS, 15)
startForegroundWithNotification()
showTripOverlay(tripDataJson, autoClose)
}
ACTION_HIDE -> dismissOverlay(reason = "programmatic")
}
return START_NOT_STICKY
}
override fun onDestroy() {
removeOverlayView()
countDownTimer?.cancel()
stopSound() // 🔴 إيقاف الصوت عند التدمير
isRunning = false
super.onDestroy()
}
// ==========================================================
// 🔴 الصوت والإشعارات 🔴
// ==========================================================
private fun playSound() {
try {
// استخدام صوت التنبيه الافتراضي القوي في الجهاز
val soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
mediaPlayer = MediaPlayer.create(this, soundUri)
mediaPlayer?.isLooping = true // تكرار الصوت حتى يتخذ السائق قراراً
mediaPlayer?.start()
} catch (e: Exception) {
Log.e(TAG, "Error playing sound: ${e.message}")
}
}
private fun stopSound() {
try {
mediaPlayer?.takeIf { it.isPlaying }?.stop()
mediaPlayer?.release()
mediaPlayer = null
} catch (e: Exception) {
Log.e(TAG, "Error stopping sound: ${e.message}")
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
"Trip Overlay Service",
NotificationManager.IMPORTANCE_LOW
)
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(channel)
}
}
private fun startForegroundWithNotification() {
val notification =
NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("طلب رحلة جديد")
.setContentText("يوجد طلب رحلة في انتظارك")
.setSmallIcon(android.R.drawable.ic_dialog_map)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
startForeground(NOTIFICATION_ID, notification)
}
// ==========================================================
// 🔴 بناء وتصميم النافذة (Premium UI) 🔴
// ==========================================================
private fun showTripOverlay(tripDataJson: String, autoCloseSeconds: Int) {
removeOverlayView()
val tripData =
parseTripData(tripDataJson)
?: run {
stopSelf()
return
}
currentTripId = tripData.tripId
val view = buildOverlayView(tripData, autoCloseSeconds)
overlayView = view
val type =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
else @Suppress("DEPRECATION") WindowManager.LayoutParams.TYPE_PHONE
val params =
WindowManager.LayoutParams(
WindowManager.LayoutParams.MATCH_PARENT,
WindowManager.LayoutParams.WRAP_CONTENT,
type,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
PixelFormat.TRANSLUCENT
)
.apply {
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
val metrics = resources.displayMetrics
y = (metrics.heightPixels * 0.15).toInt() // النزول للأسفل بنسبة 15%
}
try {
windowManager?.addView(view, params)
isRunning = true
playSound() // 🔴 تشغيل الصوت
startCountdown(autoCloseSeconds)
} catch (e: Exception) {
stopSelf()
}
}
private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
val ctx = this
// الخلفية الرئيسية بستايل الكروت الحديثة (حواف دائرية كبيرة)
val card =
FrameLayout(ctx).apply {
background =
GradientDrawable().apply {
cornerRadius = 60f
setColor(Color.WHITE)
setStroke(2, Color.parseColor("#E0E0E0")) // إطار خفيف
}
elevation = 40f
setPadding(0, 0, 0, 0)
layoutParams =
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT
)
.apply {
setMargins(40, 0, 40, 0) // هوامش جانبية
}
}
val root = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL }
card.addView(root)
// ── 1. صورة الخريطة (Static Map) ──
val mapImageView =
ImageView(ctx).apply {
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
350
) // ارتفاع الخريطة
scaleType = ImageView.ScaleType.CENTER_CROP
setBackgroundColor(Color.parseColor("#F3F4F6")) // لون رمادي فاتح كخلفية بديلة
}
root.addView(mapImageView)
// 🔴 دالة تحميل الخريطة (شغالة وتحتاج فقط مفتاحك)
loadStaticMap(mapImageView, trip.pickupLat, trip.pickupLng)
// ── حاوية التفاصيل ──
val detailsContainer =
LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(40, 30, 40, 40)
}
root.addView(detailsContainer)
// ── 2. السعر والمسافة (خط كبير وواضح) ──
val priceRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
val priceText =
TextView(ctx).apply {
text = "${trip.estimatedFare} ل.س"
textSize = 24f
setTextColor(Color.parseColor("#27AE60")) // لون أخضر فخم
typeface = android.graphics.Typeface.DEFAULT_BOLD
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
val distanceText =
TextView(ctx).apply {
text = "المسافة: ${trip.distanceKm} كم"
textSize = 14f
setTextColor(Color.parseColor("#7F8C8D"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
priceRow.addView(priceText)
priceRow.addView(distanceText)
detailsContainer.addView(priceRow)
detailsContainer.addView(divider(ctx, 30))
// ── 3. المواقع (الانطلاق والوصول) ──
detailsContainer.addView(labeledRow(ctx, "🟢 من:", trip.pickupAddress))
detailsContainer.addView(labeledRow(ctx, "🔴 إلى:", trip.dropoffAddress))
detailsContainer.addView(labeledRow(ctx, "👤 الراكب:", trip.passengerName))
detailsContainer.addView(divider(ctx, 30))
// ── 4. شريط الوقت ──
val countdownLabel =
TextView(ctx).apply {
text = "ينتهي خلال $autoCloseSeconds ثانية"
textSize = 12f
setTextColor(Color.parseColor("#E74C3C"))
gravity = Gravity.CENTER
id = android.R.id.text1
}
val progressBar =
ProgressBar(ctx, null, android.R.attr.progressBarStyleHorizontal).apply {
max = autoCloseSeconds
progress = autoCloseSeconds
id = android.R.id.progress
progressTintList =
android.content.res.ColorStateList.valueOf(Color.parseColor("#E74C3C"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 15)
.apply {
topMargin = 10
bottomMargin = 30
}
}
detailsContainer.addView(countdownLabel)
detailsContainer.addView(progressBar)
// ── 5. الأزرار (تصميم كبسولة فخم) ──
val buttonsRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL }
val rejectBtn =
Button(ctx).apply {
text = "رفض"
textSize = 16f
setTextColor(Color.parseColor("#E74C3C")) // نص أحمر
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#FDEDEC")) // خلفية حمراء باهتة فخمة
}
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = 20 }
setOnClickListener { dismissOverlay(reason = "rejected") }
}
val acceptBtn =
Button(ctx).apply {
text = "قبول الطلب"
textSize = 16f
setTextColor(Color.WHITE)
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#27AE60")) // لون أخضر فخم
}
layoutParams =
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.5f
) // زر القبول أكبر قليلاً
setOnClickListener { onTripAccepted() }
}
buttonsRow.addView(rejectBtn)
buttonsRow.addView(acceptBtn)
detailsContainer.addView(buttonsRow)
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
return card
}
// 🔴 دالة احترافية لقراءة المفتاح المحقون من Gradle 🔴
private fun getGoogleMapsApiKey(): String {
return try {
// الوصول إلى بيانات التطبيق في الـ Manifest
val appInfo =
packageManager.getApplicationInfo(
packageName,
android.content.pm.PackageManager.GET_META_DATA
)
val bundle = appInfo.metaData
// قراءة المفتاح (تأكد أن هذا هو نفس الاسم الموجود في AndroidManifest.xml لديك)
val apiKey = bundle?.getString("com.google.android.geo.API_KEY")
apiKey ?: "" // إرجاع المفتاح أو نص فارغ إذا لم يجده
} catch (e: Exception) {
Log.e(TAG, "❌ فشل في قراءة مفتاح الخريطة من Manifest: ${e.message}")
""
}
}
// ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
// ==========================================================
// ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
// ==========================================================
private fun loadStaticMap(imageView: ImageView, lat: Double, lng: Double) {
if (lat == 0.0 || lng == 0.0) {
Log.e(TAG, "⚠️ الإحداثيات صفر، تم تجاهل تحميل الخريطة")
return
}
// 🟢 جلب المفتاح بأمان تام من الـ Properties عبر الـ Manifest 🟢
val apiKey = getGoogleMapsApiKey()
if (apiKey.isEmpty()) {
Log.e(TAG, "⚠️ مفتاح جوجل ماب غير موجود أو فارغ!")
return
}
// رابط احترافي: زووم مناسب، وضع خريطة نظيف، ودبوس أخضر بارز
val mapUrl =
"https://maps.googleapis.com/maps/api/staticmap?center=$lat,$lng&zoom=15&size=600x350&maptype=roadmap&markers=color:0x27AE60%7C$lat,$lng&key=$apiKey"
thread {
try {
val url = URL(mapUrl)
val connection = url.openConnection() as HttpURLConnection
connection.doInput = true
connection.connectTimeout = 5000 // تحديد وقت أقصى للاتصال
connection.readTimeout = 5000
connection.connect()
if (connection.responseCode == HttpURLConnection.HTTP_OK) {
val bitmap = BitmapFactory.decodeStream(connection.inputStream)
// عرض الصورة في الواجهة (Main Thread)
Handler(Looper.getMainLooper()).post { imageView.setImageBitmap(bitmap) }
} else {
Log.e(TAG, "❌ خطأ من سيرفر جوجل: ${connection.responseCode}")
}
} catch (e: Exception) {
Log.e(TAG, "❌ فشل الاتصال بخدمة الخرائط: ${e.message}")
}
}
}
// ==========================================================
// 🔴 أدوات مساعدة للتصميم 🔴
// ==========================================================
private fun labeledRow(ctx: Context, label: String, value: String): LinearLayout {
return LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 8, 0, 8)
gravity = Gravity.CENTER_VERTICAL
addView(
TextView(ctx).apply {
text = label
textSize = 14f
setTextColor(Color.parseColor("#7F8C8D"))
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
.apply { rightMargin = 16 }
}
)
addView(
TextView(ctx).apply {
text = value
textSize = 15f
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
}
)
}
}
private fun statChip(ctx: Context, value: String, label: String): LinearLayout {
return LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
addView(
TextView(ctx).apply {
text = value
textSize = 16f
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
addView(
TextView(ctx).apply {
text = label
textSize = 12f
setTextColor(Color.parseColor("#95A5A6"))
}
)
}
}
private fun divider(ctx: Context, margin: Int = 16): View {
return View(ctx).apply {
setBackgroundColor(Color.parseColor("#EEEEEE"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 2).apply {
setMargins(0, margin, 0, margin)
}
}
}
// ==========================================================
// 🔴 التحكم والمؤقت 🔴
// ==========================================================
private fun startCountdown(totalSeconds: Int) {
countDownTimer?.cancel()
countDownTimer =
object : CountDownTimer(totalSeconds * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
updateCountdownUI((millisUntilFinished / 1000).toInt())
}
override fun onFinish() {
dismissOverlay(reason = "timeout")
}
}
.start()
}
private fun updateCountdownUI(secondsLeft: Int) {
val tagMap = overlayView?.tag as? Map<*, *> ?: return
val label = tagMap["countdownLabel"] as? TextView ?: return
val bar = tagMap["progressBar"] as? ProgressBar ?: return
label.text = "ينتهي خلال $secondsLeft ثانية"
bar.progress = secondsLeft
}
private fun onTripAccepted() {
stopSound() // 🔴
TripOverlayPlugin.notifyTripAccepted(currentTripId)
bringAppToForeground()
removeOverlayView()
stopSelf()
}
private fun dismissOverlay(reason: String) {
stopSound() // 🔴
countDownTimer?.cancel()
if (reason == "rejected" || reason == "timeout") {
TripOverlayPlugin.notifyTripRejected(currentTripId)
}
removeOverlayView()
stopSelf()
}
private fun bringAppToForeground() {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return
launchIntent.apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
Intent.FLAG_ACTIVITY_SINGLE_TOP
)
putExtra("acceptedTripId", currentTripId)
}
startActivity(launchIntent)
}
private fun removeOverlayView() {
overlayView?.let { windowManager?.removeView(it) }
overlayView = null
isRunning = false
}
private fun parseTripData(json: String): TripInfo? {
return try {
val obj = JSONObject(json)
TripInfo(
tripId = obj.getString("tripId"),
passengerName = obj.getString("passengerName"),
pickupAddress = obj.getString("pickupAddress"),
dropoffAddress = obj.getString("dropoffAddress"),
distanceKm = obj.getDouble("distanceKm"),
estimatedFare = obj.getDouble("estimatedFare"),
estimatedMinutes = obj.getInt("estimatedMinutes"),
pickupLat = obj.getDouble("pickupLat"),
pickupLng = obj.getDouble("pickupLng")
)
} catch (e: Exception) {
null
}
}
data class TripInfo(
val tripId: String,
val passengerName: String,
val pickupAddress: String,
val dropoffAddress: String,
val distanceKm: Double,
val estimatedFare: Double,
val estimatedMinutes: Int,
val pickupLat: Double,
val pickupLng: Double
)
}