Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-11 00:11:23 +03:00
parent 6726fcd917
commit 0ff7d04f9a
345 changed files with 3756 additions and 3369 deletions
@@ -46,6 +46,18 @@ class TripOverlayPlugin :
methodChannel?.invokeMethod("onTripRejected", mapOf("tripId" to tripId))
Log.d(TAG, "notifyTripRejected: $tripId")
}
/** Called by TripOverlayService when the driver accepts a food-delivery offer. */
fun notifyFoodOrderAccepted(orderId: String) {
methodChannel?.invokeMethod("onFoodOrderAccepted", mapOf("orderId" to orderId))
Log.d(TAG, "notifyFoodOrderAccepted: $orderId")
}
/** Called by TripOverlayService when a food offer is rejected or expires. */
fun notifyFoodOrderRejected(orderId: String) {
methodChannel?.invokeMethod("onFoodOrderRejected", mapOf("orderId" to orderId))
Log.d(TAG, "notifyFoodOrderRejected: $orderId")
}
}
// ─── FlutterPlugin ───────────────────────────────────────────────────────
@@ -85,6 +97,17 @@ class TripOverlayPlugin :
val success = showOverlay(tripDataJson, autoCloseSeconds)
result.success(success)
}
"showFoodOverlay" -> {
val foodDataJson =
call.argument<String>("foodData")
?: run {
result.error("INVALID_ARGS", "foodData is required", null)
return
}
val autoCloseSeconds = call.argument<Int>("autoCloseSeconds") ?: 20
val success = showFoodOverlay(foodDataJson, autoCloseSeconds)
result.success(success)
}
"hideOverlay" -> {
hideOverlay()
result.success(null)
@@ -141,6 +164,26 @@ class TripOverlayPlugin :
return true
}
private fun showFoodOverlay(foodDataJson: String, autoCloseSeconds: Int): Boolean {
if (!isOverlayPermissionGranted()) {
Log.w(TAG, "Overlay permission not granted")
return false
}
val intent =
Intent(context, TripOverlayService::class.java).apply {
action = TripOverlayService.ACTION_SHOW
putExtra(TripOverlayService.EXTRA_FOOD_DATA, foodDataJson)
putExtra(TripOverlayService.EXTRA_AUTO_CLOSE_SECONDS, autoCloseSeconds)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
Log.d(TAG, "showFoodOverlay called with foodData: $foodDataJson")
return true
}
private fun hideOverlay() {
val intent =
Intent(context, TripOverlayService::class.java).apply {
@@ -30,7 +30,10 @@ class TripOverlayService : Service() {
const val ACTION_SHOW = "SHOW"
const val ACTION_HIDE = "HIDE"
const val EXTRA_TRIP_DATA = "trip_data"
const val EXTRA_FOOD_DATA = "food_data"
const val EXTRA_AUTO_CLOSE_SECONDS = "auto_close_seconds"
const val MODE_TRIP = 0
const val MODE_FOOD = 1
private const val NOTIFICATION_ID = 9900
private const val CHANNEL_ID = "trip_overlay_service_channel"
@@ -43,6 +46,7 @@ class TripOverlayService : Service() {
private var overlayView: View? = null
private var countDownTimer: CountDownTimer? = null
private var currentTripId: String = ""
private var overlayMode: Int = MODE_TRIP
private var mediaPlayer: MediaPlayer? = null // 🔴 مشغل الصوت
override fun onBind(intent: Intent?): IBinder? = null
@@ -56,10 +60,18 @@ class TripOverlayService : Service() {
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, 15)
startForegroundWithNotification()
showTripOverlay(tripDataJson, autoClose)
val foodDataJson = intent.getStringExtra(EXTRA_FOOD_DATA)
if (foodDataJson != null) {
overlayMode = MODE_FOOD
startForegroundWithNotification(isFood = true)
showFoodOverlay(foodDataJson, autoClose)
} else {
val tripDataJson = intent.getStringExtra(EXTRA_TRIP_DATA) ?: return START_NOT_STICKY
overlayMode = MODE_TRIP
startForegroundWithNotification(isFood = false)
showTripOverlay(tripDataJson, autoClose)
}
}
ACTION_HIDE -> dismissOverlay(reason = "programmatic")
}
@@ -112,11 +124,13 @@ class TripOverlayService : Service() {
}
}
private fun startForegroundWithNotification() {
private fun startForegroundWithNotification(isFood: Boolean = false) {
val notification =
NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("طلب رحلة جديد")
.setContentText("يوجد طلب رحلة في انتظارك")
.setContentTitle(if (isFood) "عرض توصيل جديد" else "طلب رحلة جديد")
.setContentText(
if (isFood) "🍔 يوجد عرض توصيل في انتظارك" else "يوجد طلب رحلة في انتظارك"
)
.setSmallIcon(android.R.drawable.ic_dialog_map)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
@@ -137,7 +151,24 @@ class TripOverlayService : Service() {
}
currentTripId = tripData.tripId
val view = buildOverlayView(tripData, autoCloseSeconds)
showOverlayWindow(buildTripOverlayView(tripData, autoCloseSeconds), autoCloseSeconds)
}
private fun showFoodOverlay(foodDataJson: String, autoCloseSeconds: Int) {
removeOverlayView()
val foodData =
parseFoodData(foodDataJson)
?: run {
stopSelf()
return
}
currentTripId = foodData.orderId
showOverlayWindow(buildFoodOverlayView(foodData, autoCloseSeconds), autoCloseSeconds)
}
private fun showOverlayWindow(view: View, autoCloseSeconds: Int) {
overlayView = view
val type =
@@ -158,7 +189,7 @@ class TripOverlayService : Service() {
.apply {
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
val metrics = resources.displayMetrics
y = (metrics.heightPixels * 0.15).toInt() // النزول للأسفل بنسبة 15%
y = (metrics.heightPixels * 0.15).toInt()
}
try {
@@ -171,7 +202,7 @@ class TripOverlayService : Service() {
}
}
private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
private fun buildTripOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
val ctx = this
// الخلفية الرئيسية بستايل الكروت الحديثة (حواف دائرية كبيرة)
@@ -329,6 +360,204 @@ class TripOverlayService : Service() {
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
return card
}
// ==========================================================
// 🔴 نافذة عرض التوصيل (🍔 Food Overlay) 🔴
// ==========================================================
private fun buildFoodOverlayView(food: FoodInfo, autoCloseSeconds: Int): View {
val ctx = this
val card =
FrameLayout(ctx).apply {
background =
GradientDrawable().apply {
cornerRadius = 60f
setColor(Color.WHITE)
setStroke(2, Color.parseColor("#E0E0E0"))
}
elevation = 40f
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 }
card.addView(root)
// ── 1. رأس البطاقة: 🍔 + اسم المطعم ──
val header = LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setPadding(40, 34, 40, 20)
}
header.addView(
TextView(ctx).apply {
text = "🍔"
textSize = 34f
layoutParams =
LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply { rightMargin = 20 }
}
)
header.addView(
LinearLayout(ctx).apply {
orientation = LinearLayout.VERTICAL
addView(
TextView(ctx).apply {
text = food.merchantName
textSize = 20f
setTextColor(Color.parseColor("#2C3E50"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
if (food.merchantAddress.isNotBlank()) {
addView(
TextView(ctx).apply {
text = food.merchantAddress
textSize = 13f
setTextColor(Color.parseColor("#7F8C8D"))
maxLines = 1
ellipsize = android.text.TextUtils.TruncateAt.END
}
)
}
addView(
TextView(ctx).apply {
text = if (food.itemsCount > 0) "عدد الأصناف: ${food.itemsCount}" else ""
textSize = 13f
setTextColor(Color.parseColor("#95A5A6"))
}
)
}
)
root.addView(header)
// ── 2. رسوم التوصيل + المحتوى المستحق ──
val feeRow = LinearLayout(ctx).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setPadding(40, 0, 40, 10)
}
feeRow.addView(
TextView(ctx).apply {
text = food.deliveryFeeText.ifBlank { "—" }
textSize = 24f
setTextColor(Color.parseColor("#27AE60"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
)
if (food.isCash) {
feeRow.addView(
TextView(ctx).apply {
text = "💵 ${food.cashToCollectText.ifBlank { "—" }} قبض"
textSize = 14f
setTextColor(Color.parseColor("#E74C3C"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
}
if (food.distanceText.isNotBlank()) {
feeRow.addView(
TextView(ctx).apply {
text = food.distanceText
textSize = 14f
setTextColor(Color.parseColor("#7F8C8D"))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
)
}
root.addView(feeRow)
// ── 3. شريط الوقت ──
val countdownLabel =
TextView(ctx).apply {
text = "ينتهي خلال $autoCloseSeconds ثانية"
textSize = 12f
setTextColor(Color.parseColor("#E74C3C"))
gravity = Gravity.CENTER
id = android.R.id.text1
}
val progressBar =
ProgressBar(ctx, null, android.R.attr.progressBarStyleHorizontal).apply {
max = autoCloseSeconds
progress = autoCloseSeconds
id = android.R.id.progress
progressTintList =
android.content.res.ColorStateList.valueOf(Color.parseColor("#E74C3C"))
layoutParams =
LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 15).apply {
topMargin = 10
bottomMargin = 30
}
}
root.addView(countdownLabel)
root.addView(progressBar)
// ── 4. الأزرار ──
val buttonsRow = LinearLayout(ctx).apply { orientation = LinearLayout.HORIZONTAL }
val rejectBtn =
Button(ctx).apply {
text = "رفض"
textSize = 16f
setTextColor(Color.parseColor("#E74C3C"))
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#FDEDEC"))
}
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
.apply { rightMargin = 20 }
setOnClickListener { dismissOverlay(reason = "rejected") }
}
val acceptBtn =
Button(ctx).apply {
text = "قبول الطلب"
textSize = 16f
setTextColor(Color.WHITE)
background =
GradientDrawable().apply {
cornerRadius = 100f
setColor(Color.parseColor("#27AE60"))
}
layoutParams =
LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1.5f)
setOnClickListener { onFoodOrderAccepted() }
}
buttonsRow.addView(rejectBtn)
buttonsRow.addView(acceptBtn)
root.addView(buttonsRow)
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
return card
}
private fun parseFoodData(json: String): FoodInfo? {
return try {
val obj = JSONObject(json)
FoodInfo(
orderId = obj.getString("orderId"),
merchantName = obj.getString("merchantName"),
merchantAddress = obj.optString("merchantAddress"),
deliveryFeeText = obj.optString("deliveryFeeText"),
cashToCollectText = obj.optString("cashToCollectText"),
distanceText = obj.optString("distanceText"),
itemsCount = obj.optInt("itemsCount"),
isCash = obj.optBoolean("isCash")
)
} catch (e: Exception) {
null
}
}
// 🔴 دالة احترافية لقراءة المفتاح المحقون من Gradle 🔴
private fun getGoogleMapsApiKey(): String {
return try {
@@ -493,7 +722,15 @@ class TripOverlayService : Service() {
private fun onTripAccepted() {
stopSound() // 🔴
TripOverlayPlugin.notifyTripAccepted(currentTripId)
bringAppToForeground()
bringAppToForeground(extraKey = "acceptedTripId", extraValue = currentTripId)
removeOverlayView()
stopSelf()
}
private fun onFoodOrderAccepted() {
stopSound() // 🔴
TripOverlayPlugin.notifyFoodOrderAccepted(currentTripId)
bringAppToForeground(extraKey = "acceptedFoodOrderId", extraValue = currentTripId)
removeOverlayView()
stopSelf()
}
@@ -502,13 +739,17 @@ class TripOverlayService : Service() {
stopSound() // 🔴
countDownTimer?.cancel()
if (reason == "rejected" || reason == "timeout") {
TripOverlayPlugin.notifyTripRejected(currentTripId)
if (overlayMode == MODE_FOOD) {
TripOverlayPlugin.notifyFoodOrderRejected(currentTripId)
} else {
TripOverlayPlugin.notifyTripRejected(currentTripId)
}
}
removeOverlayView()
stopSelf()
}
private fun bringAppToForeground() {
private fun bringAppToForeground(extraKey: String, extraValue: String) {
val launchIntent = packageManager.getLaunchIntentForPackage(packageName) ?: return
launchIntent.apply {
addFlags(
@@ -516,7 +757,7 @@ class TripOverlayService : Service() {
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
Intent.FLAG_ACTIVITY_SINGLE_TOP
)
putExtra("acceptedTripId", currentTripId)
putExtra(extraKey, extraValue)
}
startActivity(launchIntent)
}
@@ -557,4 +798,15 @@ class TripOverlayService : Service() {
val pickupLat: Double,
val pickupLng: Double
)
data class FoodInfo(
val orderId: String,
val merchantName: String,
val merchantAddress: String,
val deliveryFeeText: String,
val cashToCollectText: String,
val distanceText: String,
val itemsCount: Int,
val isCash: Boolean
)
}