2026-02-21-overlay

This commit is contained in:
Hamza-Ayed
2026-02-21 13:36:25 +03:00
parent d697de9c25
commit 6f03a96e69
6 changed files with 312 additions and 204 deletions

View File

@@ -21,6 +21,9 @@ class AppLink {
static String ride = '$server/ride';
static String rideServer = 'https://rides.intaleq.xyz/intaleq/ride';
///mapOSM = 'https://routesy.intaleq.xyz'
static String mapOSM = 'https://routesy.intaleq.xyz';
static String seferCairoServer = endPoint;
static String seferGizaServer =
box.read('Giza') ?? box.read(BoxName.serverChosen);

View File

@@ -395,7 +395,12 @@ class MapDriverController extends GetxController {
}, TableName.driverOrdersRefuse);
} catch (_) {}
Get.find<HomeCaptainController>().getRefusedOrderByCaptain();
if (Get.isRegistered<HomeCaptainController>()) {
Get.find<HomeCaptainController>().getRefusedOrderByCaptain();
} else {
// في حال لم يكن مسجل (جاي من background)
Get.put(HomeCaptainController()).getRefusedOrderByCaptain();
}
if (Get.isDialogOpen == true) Get.back();
Get.offAll(
@@ -1596,7 +1601,7 @@ class MapDriverController extends GetxController {
'${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}';
// استخدام الرابط من الكلاس المرجعي لأنه أحدث
var url =
"https://routesy.intaleq.xyz/route/v1/driving/$coordinates?steps=true&overview=full";
"${AppLink.mapOSM}/route/v1/driving/$coordinates?steps=true&overview=full";
try {
var response = await http.get(Uri.parse(url));

View File

@@ -61,8 +61,7 @@ class NavigationController extends GetxController {
String distanceToNextStep = "";
// الرابط الجديد
static const String _routeApiBaseUrl =
"https://routesy.intaleq.xyz/route/v1/driving";
static final String _routeApiBaseUrl = "${AppLink.mapOSM}/route/v1/driving";
@override
void onInit() {

View File

@@ -201,6 +201,8 @@ Future<void> backgroundMessageHandler(RemoteMessage message) async {
String passengerName = title;
String pickup = 'موقع الانطلاق';
String dropoff = 'موقع الوصول';
double pLat = 0.0;
double pLng = 0.0;
if (myList.isNotEmpty && myList.length > 29) {
fare = double.tryParse(myList[26].toString()) ?? 0.0;
@@ -208,6 +210,10 @@ Future<void> backgroundMessageHandler(RemoteMessage message) async {
passengerName = myList[8].toString();
pickup = myList[29].toString();
dropoff = myList[30].toString();
// 🔴 استخراج الإحداثيات للخريطة (تأكد من الاندكس الخاص بخطوط الطول والعرض في مصفوفتك)
pLat = double.tryParse(myList[0].toString()) ?? 0.0;
pLng = double.tryParse(myList[1].toString()) ?? 0.0;
}
final tripData = TripData(
@@ -218,8 +224,8 @@ Future<void> backgroundMessageHandler(RemoteMessage message) async {
distanceKm: distance,
estimatedFare: fare,
estimatedMinutes: 0,
pickupLat: 0.0,
pickupLng: 0.0,
pickupLat: pLat, // تمرير خط العرض
pickupLng: pLng, // تمرير خط الطول
);
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;

View File

@@ -4,7 +4,7 @@ class Log {
Log._();
static void print(String value, {StackTrace? stackTrace}) {
// developer.log(value, name: 'LOG', stackTrace: stackTrace);
developer.log(value, name: 'LOG', stackTrace: stackTrace);
}
static Object? inspect(Object? object) {

View File

@@ -3,34 +3,34 @@ 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
/**
* TripOverlayService — Foreground service responsible for:
* 1. Showing a system-level overlay window (SYSTEM_ALERT_WINDOW)
* 2. Displaying trip details inside it
* 3. Handling Accept / Reject actions
* 4. Notifying Flutter via TripOverlayPlugin static callbacks
* 5. Launching the host app to foreground on accept
*/
class TripOverlayService : Service() {
companion object {
const val TAG = "TripOverlayService"
const val ACTION_SHOW = "com.intaleq_driver.SHOW"
const val ACTION_HIDE = "com.intaleq_driver.HIDE"
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"
@@ -43,8 +43,7 @@ class TripOverlayService : Service() {
private var overlayView: View? = null
private var countDownTimer: CountDownTimer? = null
private var currentTripId: String = ""
// ─── Lifecycle ────────────────────────────────────────────────────────────
private var mediaPlayer: MediaPlayer? = null // 🔴 مشغل الصوت
override fun onBind(intent: Intent?): IBinder? = null
@@ -52,20 +51,17 @@ class TripOverlayService : Service() {
super.onCreate()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
createNotificationChannel()
Log.d(TAG, "Service created")
}
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, 30)
val autoClose = intent.getIntExtra(EXTRA_AUTO_CLOSE_SECONDS, 15)
startForegroundWithNotification()
showTripOverlay(tripDataJson, autoClose)
}
ACTION_HIDE -> {
dismissOverlay(reason = "programmatic")
}
ACTION_HIDE -> dismissOverlay(reason = "programmatic")
}
return START_NOT_STICKY
}
@@ -73,26 +69,44 @@ class TripOverlayService : Service() {
override fun onDestroy() {
removeOverlayView()
countDownTimer?.cancel()
stopSound() // 🔴 إيقاف الصوت عند التدمير
isRunning = false
Log.d(TAG, "Service destroyed")
super.onDestroy()
}
// ─── Foreground Notification ──────────────────────────────────────────────
// ==========================================================
// 🔴 الصوت والإشعارات 🔴
// ==========================================================
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
)
.apply {
description = "Keeps overlay active for incoming trips"
setSound(null, null)
enableVibration(false)
}
CHANNEL_ID,
"Trip Overlay Service",
NotificationManager.IMPORTANCE_LOW
)
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(channel)
}
@@ -105,32 +119,27 @@ class TripOverlayService : Service() {
.setContentText("يوجد طلب رحلة في انتظارك")
.setSmallIcon(android.R.drawable.ic_dialog_map)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setSilent(true)
.build()
startForeground(NOTIFICATION_ID, notification)
}
// ─── Overlay Window ───────────────────────────────────────────────────────
// ==========================================================
// 🔴 بناء وتصميم النافذة (Premium UI) 🔴
// ==========================================================
private fun showTripOverlay(tripDataJson: String, autoCloseSeconds: Int) {
// Remove any existing overlay first
removeOverlayView()
// Parse trip data
val tripData =
parseTripData(tripDataJson)
?: run {
Log.e(TAG, "Failed to parse trip data")
stopSelf()
return
}
currentTripId = tripData.tripId
// Build overlay view programmatically (no XML required)
val view = buildOverlayView(tripData, autoCloseSeconds)
overlayView = view
// Window layout params
val type =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
@@ -148,79 +157,113 @@ class TripOverlayService : Service() {
)
.apply {
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
// ✅ التعديل هنا: إنزال النافذة بنسبة 18% من طول الشاشة
val metrics = resources.displayMetrics
y = (metrics.heightPixels * 0.18).toInt()
y = (metrics.heightPixels * 0.15).toInt() // النزول للأسفل بنسبة 15%
}
try {
windowManager?.addView(view, params)
isRunning = true
Log.d(TAG, "Overlay added to window manager")
playSound() // 🔴 تشغيل الصوت
startCountdown(autoCloseSeconds)
} catch (e: Exception) {
Log.e(TAG, "Failed to add overlay view: ${e.message}", e)
stopSelf()
return
}
// Start countdown timer
startCountdown(autoCloseSeconds)
}
/**
* Builds the overlay card entirely in code — no XML needed. Override this to customise the UI.
*/
private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
val ctx = this
// الخلفية الرئيسية بستايل الكروت الحديثة (حواف دائرية كبيرة)
val card =
FrameLayout(ctx).apply {
setBackgroundResource(android.R.drawable.dialog_holo_light_frame)
elevation = 24f
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
setPadding(40, 32, 40, 32)
}
val root = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL }
card.addView(root)
// ── العنوان والوقت ──
val headerRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL }
val titleText =
// ── 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 = "🚗 طلب توصيل جديد"
textSize = 18f
setTextColor(Color.parseColor("#1a1a2e"))
text = "${trip.estimatedFare} ل.س"
textSize = 24f
setTextColor(Color.parseColor("#27AE60")) // لون أخضر فخم
typeface = android.graphics.Typeface.DEFAULT_BOLD
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
headerRow.addView(titleText)
root.addView(headerRow)
root.addView(divider(ctx))
// ── بيانات الرحلة (اسم، انطلاق، مسافة، سعر) ──
// أزلنا الإيميل واكتفينا بالمعلومات المهمة
root.addView(labeledRow(ctx, "👤 الراكب:", trip.passengerName))
root.addView(labeledRow(ctx, "📍 الانطلاق:", trip.pickupAddress))
val statsRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, 16, 0, 16)
val distanceText =
TextView(ctx).apply {
text = "المسافة: ${trip.distanceKm} كم"
textSize = 14f
setTextColor(Color.parseColor("#7F8C8D"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
statsRow.addView(statChip(ctx, "${trip.distanceKm} كم", "المسافة"))
statsRow.addView(statChip(ctx, "${trip.estimatedFare} ل.س", "الأجرة"))
root.addView(statsRow)
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"))
setTextColor(Color.parseColor("#E74C3C"))
gravity = Gravity.CENTER
id = android.R.id.text1
}
@@ -230,162 +273,159 @@ class TripOverlayService : Service() {
progress = autoCloseSeconds
id = android.R.id.progress
progressTintList =
android.content.res.ColorStateList.valueOf(Color.parseColor("#e74c3c"))
}
root.addView(countdownLabel)
root.addView(progressBar)
// ── أزرار القبول والرفض ──
val buttonsRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
android.content.res.ColorStateList.valueOf(Color.parseColor("#E74C3C"))
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
.apply { topMargin = 24 }
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 = "رفض"
text = "رفض"
textSize = 16f
setTextColor(Color.WHITE)
setBackgroundColor(Color.parseColor("#e74c3c")) // لون أحمر
setTextColor(Color.parseColor("#E74C3C")) // نص أحمر
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#FDEDEC")) // خلفية حمراء باهتة فخمة
}
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = 16 }
.apply { rightMargin = 20 }
setOnClickListener { dismissOverlay(reason = "rejected") }
}
val acceptBtn =
Button(ctx).apply {
text = "قبول"
text = "قبول الطلب"
textSize = 16f
setTextColor(Color.WHITE)
setBackgroundColor(Color.parseColor("#27ae60")) // لون أخضر
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#27AE60")) // لون أخضر فخم
}
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.5f
) // زر القبول أكبر قليلاً
setOnClickListener { onTripAccepted() }
}
buttonsRow.addView(rejectBtn)
buttonsRow.addView(acceptBtn)
root.addView(buttonsRow)
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
// ─── Countdown Timer ──────────────────────────────────────────────────────
// قراءة المفتاح (تأكد أن هذا هو نفس الاسم الموجود في AndroidManifest.xml لديك)
val apiKey = bundle?.getString("com.google.android.geo.API_KEY")
private fun startCountdown(totalSeconds: Int) {
countDownTimer?.cancel()
countDownTimer =
object : CountDownTimer(totalSeconds * 1000L, 1000L) {
override fun onTick(millisUntilFinished: Long) {
val secondsLeft = (millisUntilFinished / 1000).toInt()
updateCountdownUI(secondsLeft)
}
override fun onFinish() {
Log.d(TAG, "Countdown finished — auto-dismissing overlay")
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
if (secondsLeft <= 5) {
label.setTextColor(Color.parseColor("#c0392b"))
apiKey ?: "" // إرجاع المفتاح أو نص فارغ إذا لم يجده
} catch (e: Exception) {
Log.e(TAG, "❌ فشل في قراءة مفتاح الخريطة من Manifest: ${e.message}")
""
}
}
// ─── Actions ──────────────────────────────────────────────────────────────
private fun onTripAccepted() {
Log.d(TAG, "Trip accepted: $currentTripId")
countDownTimer?.cancel()
TripOverlayPlugin.notifyTripAccepted(currentTripId)
bringAppToForeground()
removeOverlayView()
stopSelf()
}
private fun dismissOverlay(reason: String) {
Log.d(TAG, "Overlay dismissed — reason: $reason")
countDownTimer?.cancel()
if (reason == "rejected" || reason == "timeout") {
TripOverlayPlugin.notifyTripRejected(currentTripId)
// ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
// ==========================================================
// ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
// ==========================================================
private fun loadStaticMap(imageView: ImageView, lat: Double, lng: Double) {
if (lat == 0.0 || lng == 0.0) {
Log.e(TAG, "⚠️ الإحداثيات صفر، تم تجاهل تحميل الخريطة")
return
}
removeOverlayView()
stopSelf()
}
private fun bringAppToForeground() {
val packageManager = packageManager
val launchIntent =
packageManager.getLaunchIntentForPackage(packageName)
?: run {
Log.w(TAG, "No launch intent found for $packageName")
return
}
launchIntent.apply {
addFlags(
Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
Intent.FLAG_ACTIVITY_SINGLE_TOP or
Intent.FLAG_ACTIVITY_CLEAR_TOP // إضافة مهمة لتنشيط الـ Activity
)
putExtra("acceptedTripId", currentTripId)
// 🟢 جلب المفتاح بأمان تام من الـ Properties عبر الـ Manifest 🟢
val apiKey = getGoogleMapsApiKey()
if (apiKey.isEmpty()) {
Log.e(TAG, "⚠️ مفتاح جوجل ماب غير موجود أو فارغ!")
return
}
startActivity(launchIntent)
Log.d(TAG, "Launched app to foreground")
}
// ─── Helpers ──────────────────────────────────────────────────────────────
// رابط احترافي: زووم مناسب، وضع خريطة نظيف، ودبوس أخضر بارز
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"
private fun removeOverlayView() {
overlayView?.let {
thread {
try {
windowManager?.removeView(it)
Log.d(TAG, "Overlay view removed")
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, "Error removing overlay view: ${e.message}")
Log.e(TAG, "❌ فشل الاتصال بخدمة الخرائط: ${e.message}")
}
overlayView = null
isRunning = false
}
}
// ==========================================================
// 🔴 أدوات مساعدة للتصميم 🔴
// ==========================================================
private fun labeledRow(ctx: Context, label: String, value: String): LinearLayout {
return LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(0, 12, 0, 4)
orientation = LinearLayout.HORIZONTAL
setPadding(0, 8, 0, 8)
gravity = Gravity.CENTER_VERTICAL
addView(
TextView(ctx).apply {
text = label
textSize = 11f
setTextColor(Color.parseColor("#888888"))
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 = 14f
setTextColor(Color.parseColor("#1a1a2e"))
textSize = 15f
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
}
)
}
@@ -396,40 +436,96 @@ class TripOverlayService : Service() {
orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setPadding(12, 16, 12, 16)
setBackgroundColor(Color.parseColor("#f0f4f8"))
addView(
TextView(ctx).apply {
text = value
textSize = 16f
setTextColor(Color.parseColor("#2c3e50"))
gravity = Gravity.CENTER
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
addView(
TextView(ctx).apply {
text = label
textSize = 10f
setTextColor(Color.parseColor("#7f8c8d"))
gravity = Gravity.CENTER
textSize = 12f
setTextColor(Color.parseColor("#95A5A6"))
}
)
}
}
private fun divider(ctx: Context): View {
private fun divider(ctx: Context, margin: Int = 16): View {
return View(ctx).apply {
setBackgroundColor(Color.parseColor("#e0e0e0"))
setBackgroundColor(Color.parseColor("#EEEEEE"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply {
setMargins(0, 16, 0, 16)
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 2).apply {
setMargins(0, margin, 0, margin)
}
}
}
// ─── Data Parsing ─────────────────────────────────────────────────────────
// ==========================================================
// 🔴 التحكم والمؤقت 🔴
// ==========================================================
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 {
@@ -443,10 +539,9 @@ class TripOverlayService : Service() {
estimatedFare = obj.getDouble("estimatedFare"),
estimatedMinutes = obj.getInt("estimatedMinutes"),
pickupLat = obj.getDouble("pickupLat"),
pickupLng = obj.getDouble("pickupLng"),
pickupLng = obj.getDouble("pickupLng")
)
} catch (e: Exception) {
Log.e(TAG, "JSON parse error: ${e.message}")
null
}
}
@@ -460,6 +555,6 @@ class TripOverlayService : Service() {
val estimatedFare: Double,
val estimatedMinutes: Int,
val pickupLat: Double,
val pickupLng: Double,
val pickupLng: Double
)
}