From 6f03a96e69e4cc65ff1e183dc4333f2a54f62214 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 21 Feb 2026 13:36:25 +0300 Subject: [PATCH] 2026-02-21-overlay --- lib/constant/links.dart | 3 + .../home/captin/map_driver_controller.dart | 9 +- .../navigation/navigation_controller.dart | 3 +- lib/main.dart | 10 +- lib/print.dart | 2 +- .../TripzOverlayService.kt | 489 +++++++++++------- 6 files changed, 312 insertions(+), 204 deletions(-) diff --git a/lib/constant/links.dart b/lib/constant/links.dart index d8e3d89..e2c8ae9 100755 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -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); diff --git a/lib/controller/home/captin/map_driver_controller.dart b/lib/controller/home/captin/map_driver_controller.dart index 733cf3b..176082c 100755 --- a/lib/controller/home/captin/map_driver_controller.dart +++ b/lib/controller/home/captin/map_driver_controller.dart @@ -395,7 +395,12 @@ class MapDriverController extends GetxController { }, TableName.driverOrdersRefuse); } catch (_) {} - Get.find().getRefusedOrderByCaptain(); + if (Get.isRegistered()) { + Get.find().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)); diff --git a/lib/controller/home/navigation/navigation_controller.dart b/lib/controller/home/navigation/navigation_controller.dart index f8f0a76..8e4b79e 100644 --- a/lib/controller/home/navigation/navigation_controller.dart +++ b/lib/controller/home/navigation/navigation_controller.dart @@ -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() { diff --git a/lib/main.dart b/lib/main.dart index 7b72d75..6bddc31 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -201,6 +201,8 @@ Future 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 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 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; diff --git a/lib/print.dart b/lib/print.dart index 63efb2d..a3d59f6 100755 --- a/lib/print.dart +++ b/lib/print.dart @@ -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) { diff --git a/trip_overlay_plugin/android/src/main/kotlin/com/intaleq_driver/trip_overlay_plugin/TripzOverlayService.kt b/trip_overlay_plugin/android/src/main/kotlin/com/intaleq_driver/trip_overlay_plugin/TripzOverlayService.kt index aa08527..1811c3b 100644 --- a/trip_overlay_plugin/android/src/main/kotlin/com/intaleq_driver/trip_overlay_plugin/TripzOverlayService.kt +++ b/trip_overlay_plugin/android/src/main/kotlin/com/intaleq_driver/trip_overlay_plugin/TripzOverlayService.kt @@ -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 ) }