2026-02-21-overlay
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -395,7 +395,12 @@ class MapDriverController extends GetxController {
|
||||
}, TableName.driverOrdersRefuse);
|
||||
} catch (_) {}
|
||||
|
||||
Get.find<HomeCaptainController>().getRefusedOrderByCaptain();
|
||||
if (Get.isRegistered<HomeCaptainController>()) {
|
||||
Get.find<HomeCaptainController>().getRefusedOrderByCaptain();
|
||||
} else {
|
||||
// في حال لم يكن مسجل (جاي من background)
|
||||
Get.put(HomeCaptainController()).getRefusedOrderByCaptain();
|
||||
}
|
||||
|
||||
if (Get.isDialogOpen == true) Get.back();
|
||||
Get.offAll(
|
||||
@@ -1596,7 +1601,7 @@ class MapDriverController extends GetxController {
|
||||
'${origin.longitude},${origin.latitude};${destination.longitude},${destination.latitude}';
|
||||
// استخدام الرابط من الكلاس المرجعي لأنه أحدث
|
||||
var url =
|
||||
"https://routesy.intaleq.xyz/route/v1/driving/$coordinates?steps=true&overview=full";
|
||||
"${AppLink.mapOSM}/route/v1/driving/$coordinates?steps=true&overview=full";
|
||||
|
||||
try {
|
||||
var response = await http.get(Uri.parse(url));
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -201,6 +201,8 @@ Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
||||
String passengerName = title;
|
||||
String pickup = 'موقع الانطلاق';
|
||||
String dropoff = 'موقع الوصول';
|
||||
double pLat = 0.0;
|
||||
double pLng = 0.0;
|
||||
|
||||
if (myList.isNotEmpty && myList.length > 29) {
|
||||
fare = double.tryParse(myList[26].toString()) ?? 0.0;
|
||||
@@ -208,6 +210,10 @@ Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
||||
passengerName = myList[8].toString();
|
||||
pickup = myList[29].toString();
|
||||
dropoff = myList[30].toString();
|
||||
|
||||
// 🔴 استخراج الإحداثيات للخريطة (تأكد من الاندكس الخاص بخطوط الطول والعرض في مصفوفتك)
|
||||
pLat = double.tryParse(myList[0].toString()) ?? 0.0;
|
||||
pLng = double.tryParse(myList[1].toString()) ?? 0.0;
|
||||
}
|
||||
|
||||
final tripData = TripData(
|
||||
@@ -218,8 +224,8 @@ Future<void> backgroundMessageHandler(RemoteMessage message) async {
|
||||
distanceKm: distance,
|
||||
estimatedFare: fare,
|
||||
estimatedMinutes: 0,
|
||||
pickupLat: 0.0,
|
||||
pickupLng: 0.0,
|
||||
pickupLat: pLat, // تمرير خط العرض
|
||||
pickupLng: pLng, // تمرير خط الطول
|
||||
);
|
||||
|
||||
bool isAppInForeground = box.read(BoxName.isAppInForeground) ?? false;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user