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 ride = '$server/ride';
static String rideServer = 'https://rides.intaleq.xyz/intaleq/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 seferCairoServer = endPoint;
static String seferGizaServer = static String seferGizaServer =
box.read('Giza') ?? box.read(BoxName.serverChosen); box.read('Giza') ?? box.read(BoxName.serverChosen);

View File

@@ -395,7 +395,12 @@ class MapDriverController extends GetxController {
}, TableName.driverOrdersRefuse); }, TableName.driverOrdersRefuse);
} catch (_) {} } 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(); if (Get.isDialogOpen == true) Get.back();
Get.offAll( Get.offAll(
@@ -1596,7 +1601,7 @@ class MapDriverController extends GetxController {
'${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}'; '${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}';
// استخدام الرابط من الكلاس المرجعي لأنه أحدث // استخدام الرابط من الكلاس المرجعي لأنه أحدث
var url = 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 { try {
var response = await http.get(Uri.parse(url)); var response = await http.get(Uri.parse(url));

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ class Log {
Log._(); Log._();
static void print(String value, {StackTrace? stackTrace}) { 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) { static Object? inspect(Object? object) {

View File

@@ -3,34 +3,34 @@ package com.intaleq_driver.trip_overlay_plugin
import android.app.* import android.app.*
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.BitmapFactory
import android.graphics.Color import android.graphics.Color
import android.graphics.PixelFormat import android.graphics.PixelFormat
import android.graphics.drawable.GradientDrawable
import android.media.MediaPlayer
import android.media.RingtoneManager
import android.os.Build import android.os.Build
import android.os.CountDownTimer import android.os.CountDownTimer
import android.os.Handler
import android.os.IBinder import android.os.IBinder
import android.os.Looper
import android.util.Log import android.util.Log
import android.view.* import android.view.*
import android.widget.* import android.widget.*
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import java.net.HttpURLConnection
import java.net.URL
import kotlin.concurrent.thread
import org.json.JSONObject 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() { class TripOverlayService : Service() {
companion object { companion object {
const val TAG = "TripOverlayService" const val TAG = "TripOverlayService"
const val ACTION_SHOW = "com.intaleq_driver.SHOW" const val ACTION_SHOW = "SHOW"
const val ACTION_HIDE = "com.intaleq_driver.HIDE" const val ACTION_HIDE = "HIDE"
const val EXTRA_TRIP_DATA = "trip_data" const val EXTRA_TRIP_DATA = "trip_data"
const val EXTRA_AUTO_CLOSE_SECONDS = "auto_close_seconds" const val EXTRA_AUTO_CLOSE_SECONDS = "auto_close_seconds"
private const val NOTIFICATION_ID = 9900 private const val NOTIFICATION_ID = 9900
private const val CHANNEL_ID = "trip_overlay_service_channel" private const val CHANNEL_ID = "trip_overlay_service_channel"
@@ -43,8 +43,7 @@ class TripOverlayService : Service() {
private var overlayView: View? = null private var overlayView: View? = null
private var countDownTimer: CountDownTimer? = null private var countDownTimer: CountDownTimer? = null
private var currentTripId: String = "" private var currentTripId: String = ""
private var mediaPlayer: MediaPlayer? = null // 🔴 مشغل الصوت
// ─── Lifecycle ────────────────────────────────────────────────────────────
override fun onBind(intent: Intent?): IBinder? = null override fun onBind(intent: Intent?): IBinder? = null
@@ -52,20 +51,17 @@ class TripOverlayService : Service() {
super.onCreate() super.onCreate()
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
createNotificationChannel() createNotificationChannel()
Log.d(TAG, "Service created")
} }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) { when (intent?.action) {
ACTION_SHOW -> { ACTION_SHOW -> {
val tripDataJson = intent.getStringExtra(EXTRA_TRIP_DATA) ?: return START_NOT_STICKY 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() startForegroundWithNotification()
showTripOverlay(tripDataJson, autoClose) showTripOverlay(tripDataJson, autoClose)
} }
ACTION_HIDE -> { ACTION_HIDE -> dismissOverlay(reason = "programmatic")
dismissOverlay(reason = "programmatic")
}
} }
return START_NOT_STICKY return START_NOT_STICKY
} }
@@ -73,26 +69,44 @@ class TripOverlayService : Service() {
override fun onDestroy() { override fun onDestroy() {
removeOverlayView() removeOverlayView()
countDownTimer?.cancel() countDownTimer?.cancel()
stopSound() // 🔴 إيقاف الصوت عند التدمير
isRunning = false isRunning = false
Log.d(TAG, "Service destroyed")
super.onDestroy() 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() { private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = val channel =
NotificationChannel( NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
"Trip Overlay Service", "Trip Overlay Service",
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
) )
.apply {
description = "Keeps overlay active for incoming trips"
setSound(null, null)
enableVibration(false)
}
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager) (getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(channel) .createNotificationChannel(channel)
} }
@@ -105,32 +119,27 @@ class TripOverlayService : Service() {
.setContentText("يوجد طلب رحلة في انتظارك") .setContentText("يوجد طلب رحلة في انتظارك")
.setSmallIcon(android.R.drawable.ic_dialog_map) .setSmallIcon(android.R.drawable.ic_dialog_map)
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setSilent(true)
.build() .build()
startForeground(NOTIFICATION_ID, notification) startForeground(NOTIFICATION_ID, notification)
} }
// ─── Overlay Window ─────────────────────────────────────────────────────── // ==========================================================
// 🔴 بناء وتصميم النافذة (Premium UI) 🔴
// ==========================================================
private fun showTripOverlay(tripDataJson: String, autoCloseSeconds: Int) { private fun showTripOverlay(tripDataJson: String, autoCloseSeconds: Int) {
// Remove any existing overlay first
removeOverlayView() removeOverlayView()
// Parse trip data
val tripData = val tripData =
parseTripData(tripDataJson) parseTripData(tripDataJson)
?: run { ?: run {
Log.e(TAG, "Failed to parse trip data")
stopSelf() stopSelf()
return return
} }
currentTripId = tripData.tripId currentTripId = tripData.tripId
// Build overlay view programmatically (no XML required)
val view = buildOverlayView(tripData, autoCloseSeconds) val view = buildOverlayView(tripData, autoCloseSeconds)
overlayView = view overlayView = view
// Window layout params
val type = val type =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
@@ -148,79 +157,113 @@ class TripOverlayService : Service() {
) )
.apply { .apply {
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
// ✅ التعديل هنا: إنزال النافذة بنسبة 18% من طول الشاشة
val metrics = resources.displayMetrics val metrics = resources.displayMetrics
y = (metrics.heightPixels * 0.18).toInt() y = (metrics.heightPixels * 0.15).toInt() // النزول للأسفل بنسبة 15%
} }
try { try {
windowManager?.addView(view, params) windowManager?.addView(view, params)
isRunning = true isRunning = true
Log.d(TAG, "Overlay added to window manager") playSound() // 🔴 تشغيل الصوت
startCountdown(autoCloseSeconds)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to add overlay view: ${e.message}", e)
stopSelf() 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 { private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
val ctx = this val ctx = this
// الخلفية الرئيسية بستايل الكروت الحديثة (حواف دائرية كبيرة)
val card = val card =
FrameLayout(ctx).apply { FrameLayout(ctx).apply {
setBackgroundResource(android.R.drawable.dialog_holo_light_frame) background =
elevation = 24f 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 = val root = LinearLayout(ctx).apply { orientation = LinearLayout.VERTICAL }
LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
setPadding(40, 32, 40, 32)
}
card.addView(root) card.addView(root)
// ── العنوان والوقت ── // ── 1. صورة الخريطة (Static Map) ──
val headerRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL } val mapImageView =
val titleText = 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 { TextView(ctx).apply {
text = "🚗 طلب توصيل جديد" text = "${trip.estimatedFare} ل.س"
textSize = 18f textSize = 24f
setTextColor(Color.parseColor("#1a1a2e")) setTextColor(Color.parseColor("#27AE60")) // لون أخضر فخم
typeface = android.graphics.Typeface.DEFAULT_BOLD typeface = android.graphics.Typeface.DEFAULT_BOLD
layoutParams = layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
} }
headerRow.addView(titleText) val distanceText =
root.addView(headerRow) TextView(ctx).apply {
root.addView(divider(ctx)) text = "المسافة: ${trip.distanceKm} كم"
textSize = 14f
// ── بيانات الرحلة (اسم، انطلاق، مسافة، سعر) ── setTextColor(Color.parseColor("#7F8C8D"))
// أزلنا الإيميل واكتفينا بالمعلومات المهمة typeface = android.graphics.Typeface.DEFAULT_BOLD
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)
} }
statsRow.addView(statChip(ctx, "${trip.distanceKm} كم", "المسافة")) priceRow.addView(priceText)
statsRow.addView(statChip(ctx, "${trip.estimatedFare} ل.س", "الأجرة")) priceRow.addView(distanceText)
root.addView(statsRow) 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 = val countdownLabel =
TextView(ctx).apply { TextView(ctx).apply {
text = "ينتهي خلال $autoCloseSeconds ثانية" text = "ينتهي خلال $autoCloseSeconds ثانية"
textSize = 12f textSize = 12f
setTextColor(Color.parseColor("#e74c3c")) setTextColor(Color.parseColor("#E74C3C"))
gravity = Gravity.CENTER gravity = Gravity.CENTER
id = android.R.id.text1 id = android.R.id.text1
} }
@@ -230,162 +273,159 @@ class TripOverlayService : Service() {
progress = autoCloseSeconds progress = autoCloseSeconds
id = android.R.id.progress id = android.R.id.progress
progressTintList = progressTintList =
android.content.res.ColorStateList.valueOf(Color.parseColor("#e74c3c")) android.content.res.ColorStateList.valueOf(Color.parseColor("#E74C3C"))
}
root.addView(countdownLabel)
root.addView(progressBar)
// ── أزرار القبول والرفض ──
val buttonsRow =
LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = layoutParams =
LinearLayout.LayoutParams( LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 15)
LinearLayout.LayoutParams.MATCH_PARENT, .apply {
LinearLayout.LayoutParams.WRAP_CONTENT topMargin = 10
) bottomMargin = 30
.apply { topMargin = 24 } }
} }
detailsContainer.addView(countdownLabel)
detailsContainer.addView(progressBar)
// ── 5. الأزرار (تصميم كبسولة فخم) ──
val buttonsRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL }
val rejectBtn = val rejectBtn =
Button(ctx).apply { Button(ctx).apply {
text = "رفض" text = "رفض"
textSize = 16f textSize = 16f
setTextColor(Color.WHITE) setTextColor(Color.parseColor("#E74C3C")) // نص أحمر
setBackgroundColor(Color.parseColor("#e74c3c")) // لون أحمر background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#FDEDEC")) // خلفية حمراء باهتة فخمة
}
layoutParams = layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = 16 } .apply { rightMargin = 20 }
setOnClickListener { dismissOverlay(reason = "rejected") } setOnClickListener { dismissOverlay(reason = "rejected") }
} }
val acceptBtn = val acceptBtn =
Button(ctx).apply { Button(ctx).apply {
text = "قبول" text = "قبول الطلب"
textSize = 16f textSize = 16f
setTextColor(Color.WHITE) setTextColor(Color.WHITE)
setBackgroundColor(Color.parseColor("#27ae60")) // لون أخضر background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#27AE60")) // لون أخضر فخم
}
layoutParams = layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1.5f
) // زر القبول أكبر قليلاً
setOnClickListener { onTripAccepted() } setOnClickListener { onTripAccepted() }
} }
buttonsRow.addView(rejectBtn) buttonsRow.addView(rejectBtn)
buttonsRow.addView(acceptBtn) buttonsRow.addView(acceptBtn)
root.addView(buttonsRow) detailsContainer.addView(buttonsRow)
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar) card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
return card 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) { apiKey ?: "" // إرجاع المفتاح أو نص فارغ إذا لم يجده
countDownTimer?.cancel() } catch (e: Exception) {
countDownTimer = Log.e(TAG, "❌ فشل في قراءة مفتاح الخريطة من Manifest: ${e.message}")
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"))
} }
} }
// ─── Actions ────────────────────────────────────────────────────────────── // ==========================================================
// 🔴 جلب صورة الخريطة (Static Map) 🔴
private fun onTripAccepted() { // ==========================================================
Log.d(TAG, "Trip accepted: $currentTripId") // ==========================================================
countDownTimer?.cancel() // 🔴 جلب صورة الخريطة (Static Map) 🔴
TripOverlayPlugin.notifyTripAccepted(currentTripId) // ==========================================================
bringAppToForeground() private fun loadStaticMap(imageView: ImageView, lat: Double, lng: Double) {
removeOverlayView() if (lat == 0.0 || lng == 0.0) {
stopSelf() Log.e(TAG, "⚠️ الإحداثيات صفر، تم تجاهل تحميل الخريطة")
} return
private fun dismissOverlay(reason: String) {
Log.d(TAG, "Overlay dismissed — reason: $reason")
countDownTimer?.cancel()
if (reason == "rejected" || reason == "timeout") {
TripOverlayPlugin.notifyTripRejected(currentTripId)
} }
removeOverlayView()
stopSelf()
}
private fun bringAppToForeground() { // 🟢 جلب المفتاح بأمان تام من الـ Properties عبر الـ Manifest 🟢
val packageManager = packageManager val apiKey = getGoogleMapsApiKey()
val launchIntent =
packageManager.getLaunchIntentForPackage(packageName) if (apiKey.isEmpty()) {
?: run { Log.e(TAG, "⚠️ مفتاح جوجل ماب غير موجود أو فارغ!")
Log.w(TAG, "No launch intent found for $packageName") return
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)
} }
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() { thread {
overlayView?.let {
try { try {
windowManager?.removeView(it) val url = URL(mapUrl)
Log.d(TAG, "Overlay view removed") 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) { } 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 { private fun labeledRow(ctx: Context, label: String, value: String): LinearLayout {
return LinearLayout(ctx).apply { return LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.HORIZONTAL
setPadding(0, 12, 0, 4) setPadding(0, 8, 0, 8)
gravity = Gravity.CENTER_VERTICAL
addView( addView(
TextView(ctx).apply { TextView(ctx).apply {
text = label text = label
textSize = 11f textSize = 14f
setTextColor(Color.parseColor("#888888")) setTextColor(Color.parseColor("#7F8C8D"))
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
.apply { rightMargin = 16 }
} }
) )
addView( addView(
TextView(ctx).apply { TextView(ctx).apply {
text = value text = value
textSize = 14f textSize = 15f
setTextColor(Color.parseColor("#1a1a2e")) setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD typeface = android.graphics.Typeface.DEFAULT_BOLD
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
} }
) )
} }
@@ -396,40 +436,96 @@ class TripOverlayService : Service() {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
gravity = Gravity.CENTER gravity = Gravity.CENTER
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
setPadding(12, 16, 12, 16)
setBackgroundColor(Color.parseColor("#f0f4f8"))
addView( addView(
TextView(ctx).apply { TextView(ctx).apply {
text = value text = value
textSize = 16f textSize = 16f
setTextColor(Color.parseColor("#2c3e50")) setTextColor(Color.parseColor("#2C3E50"))
gravity = Gravity.CENTER
typeface = android.graphics.Typeface.DEFAULT_BOLD typeface = android.graphics.Typeface.DEFAULT_BOLD
} }
) )
addView( addView(
TextView(ctx).apply { TextView(ctx).apply {
text = label text = label
textSize = 10f textSize = 12f
setTextColor(Color.parseColor("#7f8c8d")) setTextColor(Color.parseColor("#95A5A6"))
gravity = Gravity.CENTER
} }
) )
} }
} }
private fun divider(ctx: Context): View { private fun divider(ctx: Context, margin: Int = 16): View {
return View(ctx).apply { return View(ctx).apply {
setBackgroundColor(Color.parseColor("#e0e0e0")) setBackgroundColor(Color.parseColor("#EEEEEE"))
layoutParams = layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 1).apply { LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 2).apply {
setMargins(0, 16, 0, 16) 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? { private fun parseTripData(json: String): TripInfo? {
return try { return try {
@@ -443,10 +539,9 @@ class TripOverlayService : Service() {
estimatedFare = obj.getDouble("estimatedFare"), estimatedFare = obj.getDouble("estimatedFare"),
estimatedMinutes = obj.getInt("estimatedMinutes"), estimatedMinutes = obj.getInt("estimatedMinutes"),
pickupLat = obj.getDouble("pickupLat"), pickupLat = obj.getDouble("pickupLat"),
pickupLng = obj.getDouble("pickupLng"), pickupLng = obj.getDouble("pickupLng")
) )
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "JSON parse error: ${e.message}")
null null
} }
} }
@@ -460,6 +555,6 @@ class TripOverlayService : Service() {
val estimatedFare: Double, val estimatedFare: Double,
val estimatedMinutes: Int, val estimatedMinutes: Int,
val pickupLat: Double, val pickupLat: Double,
val pickupLng: Double, val pickupLng: Double
) )
} }