2026-02-21-overlay
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user