Food: background location + native delivery overlay
This commit is contained in:
+41
@@ -46,6 +46,17 @@ class TripOverlayPlugin :
|
||||
methodChannel?.invokeMethod("onTripRejected", mapOf("tripId" to tripId))
|
||||
Log.d(TAG, "notifyTripRejected: $tripId")
|
||||
}
|
||||
|
||||
/** طلبات توصيل الطعام — قناة منفصلة عن الرحلات في جهة Dart أيضاً */
|
||||
fun notifyFoodOrderAccepted(orderId: String) {
|
||||
methodChannel?.invokeMethod("onFoodOrderAccepted", mapOf("orderId" to orderId))
|
||||
Log.d(TAG, "notifyFoodOrderAccepted: $orderId")
|
||||
}
|
||||
|
||||
fun notifyFoodOrderRejected(orderId: String) {
|
||||
methodChannel?.invokeMethod("onFoodOrderRejected", mapOf("orderId" to orderId))
|
||||
Log.d(TAG, "notifyFoodOrderRejected: $orderId")
|
||||
}
|
||||
}
|
||||
|
||||
// ─── FlutterPlugin ───────────────────────────────────────────────────────
|
||||
@@ -85,6 +96,16 @@ 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
|
||||
result.success(showFoodOverlay(foodDataJson, autoCloseSeconds))
|
||||
}
|
||||
"hideOverlay" -> {
|
||||
hideOverlay()
|
||||
result.success(null)
|
||||
@@ -141,6 +162,26 @@ class TripOverlayPlugin :
|
||||
return true
|
||||
}
|
||||
|
||||
private fun showFoodOverlay(foodDataJson: String, autoCloseSeconds: Int): Boolean {
|
||||
return try {
|
||||
val intent =
|
||||
Intent(context, TripOverlayService::class.java).apply {
|
||||
action = TripOverlayService.ACTION_SHOW_FOOD
|
||||
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)
|
||||
}
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "showFoodOverlay failed: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideOverlay() {
|
||||
val intent =
|
||||
Intent(context, TripOverlayService::class.java).apply {
|
||||
|
||||
+306
-2
@@ -28,8 +28,10 @@ class TripOverlayService : Service() {
|
||||
companion object {
|
||||
const val TAG = "TripOverlayService"
|
||||
const val ACTION_SHOW = "SHOW"
|
||||
const val ACTION_SHOW_FOOD = "SHOW_FOOD"
|
||||
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"
|
||||
private const val NOTIFICATION_ID = 9900
|
||||
private const val CHANNEL_ID = "trip_overlay_service_channel"
|
||||
@@ -43,6 +45,9 @@ class TripOverlayService : Service() {
|
||||
private var overlayView: View? = null
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
private var currentTripId: String = ""
|
||||
private var currentFoodOrderId: String = ""
|
||||
// نوع النافذة المعروضة الآن — يحدّد لمن نُرسل نتيجة القبول/الرفض
|
||||
private var isFoodOverlay: Boolean = false
|
||||
private var mediaPlayer: MediaPlayer? = null // 🔴 مشغل الصوت
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
@@ -61,6 +66,11 @@ class TripOverlayService : Service() {
|
||||
startForegroundWithNotification()
|
||||
showTripOverlay(tripDataJson, autoClose)
|
||||
}
|
||||
ACTION_SHOW_FOOD -> {
|
||||
val foodDataJson = intent.getStringExtra(EXTRA_FOOD_DATA) ?: return START_NOT_STICKY
|
||||
val autoClose = intent.getIntExtra(EXTRA_AUTO_CLOSE_SECONDS, 20)
|
||||
showFoodOverlay(foodDataJson, autoClose)
|
||||
}
|
||||
ACTION_HIDE -> dismissOverlay(reason = "programmatic")
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
@@ -171,6 +181,291 @@ class TripOverlayService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// نافذة طلبات التوصيل — تصميم مستقل عن نافذة الرحلة، ويشترك معها في
|
||||
// البنية المثبتة فقط (الصوت، العدّاد، صلاحية النافذة، الخدمة الأمامية).
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
private fun showFoodOverlay(foodDataJson: String, autoCloseSeconds: Int) {
|
||||
removeOverlayView()
|
||||
|
||||
val food =
|
||||
parseFoodData(foodDataJson)
|
||||
?: run {
|
||||
stopSelf()
|
||||
return
|
||||
}
|
||||
currentFoodOrderId = food.orderId
|
||||
isFoodOverlay = true
|
||||
|
||||
val view = buildFoodOverlayView(food, autoCloseSeconds)
|
||||
overlayView = view
|
||||
|
||||
val type =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
|
||||
else @Suppress("DEPRECATION") WindowManager.LayoutParams.TYPE_PHONE
|
||||
|
||||
val params =
|
||||
WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
type,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
|
||||
WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
|
||||
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
|
||||
PixelFormat.TRANSLUCENT
|
||||
)
|
||||
.apply {
|
||||
gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL
|
||||
val metrics = resources.displayMetrics
|
||||
y = (metrics.heightPixels * 0.15).toInt()
|
||||
}
|
||||
|
||||
try {
|
||||
windowManager?.addView(view, params)
|
||||
isRunning = true
|
||||
playSound()
|
||||
startCountdown(autoCloseSeconds)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Food overlay failed: ${e.message}")
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildFoodOverlayView(food: FoodInfo, autoCloseSeconds: Int): View {
|
||||
val ctx = this
|
||||
val orange = Color.parseColor("#FF6B35")
|
||||
|
||||
val card =
|
||||
FrameLayout(ctx).apply {
|
||||
background =
|
||||
GradientDrawable().apply {
|
||||
cornerRadius = 60f
|
||||
setColor(Color.WHITE)
|
||||
setStroke(3, orange)
|
||||
}
|
||||
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)
|
||||
|
||||
// ── ترويسة برتقالية: هوية بصرية تفصل التوصيل عن الرحلة من النظرة الأولى ──
|
||||
root.addView(
|
||||
LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
background =
|
||||
GradientDrawable().apply {
|
||||
cornerRadii =
|
||||
floatArrayOf(60f, 60f, 60f, 60f, 0f, 0f, 0f, 0f)
|
||||
setColor(orange)
|
||||
}
|
||||
setPadding(40, 34, 40, 34)
|
||||
addView(
|
||||
TextView(ctx).apply {
|
||||
text = "\uD83C\uDF54 طلب توصيل جديد"
|
||||
textSize = 17f
|
||||
setTextColor(Color.WHITE)
|
||||
gravity = Gravity.CENTER
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
)
|
||||
addView(
|
||||
TextView(ctx).apply {
|
||||
text = food.deliveryFeeText
|
||||
textSize = 30f
|
||||
setTextColor(Color.WHITE)
|
||||
gravity = Gravity.CENTER
|
||||
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
||||
}
|
||||
)
|
||||
addView(
|
||||
TextView(ctx).apply {
|
||||
text = "أجرة التوصيل"
|
||||
textSize = 12f
|
||||
setTextColor(Color.parseColor("#FFE0D2"))
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
val body =
|
||||
LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(40, 30, 40, 30)
|
||||
}
|
||||
root.addView(body)
|
||||
|
||||
// ── المطعم ووجهة التسليم ──
|
||||
body.addView(labeledRow(ctx, "\uD83C\uDFEA المطعم", food.merchantName))
|
||||
if (food.merchantAddress.isNotEmpty()) {
|
||||
body.addView(labeledRow(ctx, "\uD83D\uDCCD العنوان", food.merchantAddress))
|
||||
}
|
||||
if (food.distanceText.isNotEmpty()) {
|
||||
body.addView(labeledRow(ctx, "\uD83D\uDEE3\uFE0F المسافة", food.distanceText))
|
||||
}
|
||||
if (food.itemsCount > 0) {
|
||||
body.addView(labeledRow(ctx, "\uD83E\uDDFE الأصناف", "${food.itemsCount}"))
|
||||
}
|
||||
body.addView(
|
||||
labeledRow(ctx, "\uD83D\uDCB3 الدفع", if (food.isCash) "نقداً" else "محفظة")
|
||||
)
|
||||
|
||||
// ── تنبيه التحصيل النقدي: قرار مالي يحتاجه السائق قبل القبول لا بعده ──
|
||||
if (food.isCash && food.cashToCollectText.isNotEmpty()) {
|
||||
body.addView(
|
||||
TextView(ctx).apply {
|
||||
text = "\uD83D\uDCB5 تحصّل من الزبون: ${food.cashToCollectText}"
|
||||
textSize = 13f
|
||||
setTextColor(Color.parseColor("#8A6100"))
|
||||
background =
|
||||
GradientDrawable().apply {
|
||||
cornerRadius = 24f
|
||||
setColor(Color.parseColor("#FFF4DC"))
|
||||
}
|
||||
setPadding(24, 18, 24, 18)
|
||||
layoutParams =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
.apply { topMargin = 16 }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ── العدّاد ──
|
||||
val countdownLabel =
|
||||
TextView(ctx).apply {
|
||||
text = "ينتهي خلال $autoCloseSeconds ثانية"
|
||||
textSize = 13f
|
||||
setTextColor(Color.parseColor("#7F8C8D"))
|
||||
gravity = Gravity.CENTER
|
||||
layoutParams =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
.apply { topMargin = 24 }
|
||||
}
|
||||
val progressBar =
|
||||
ProgressBar(ctx, null, android.R.attr.progressBarStyleHorizontal).apply {
|
||||
max = autoCloseSeconds
|
||||
progress = autoCloseSeconds
|
||||
progressDrawable?.colorFilter =
|
||||
android.graphics.PorterDuffColorFilter(
|
||||
orange,
|
||||
android.graphics.PorterDuff.Mode.SRC_IN
|
||||
)
|
||||
layoutParams =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
15
|
||||
)
|
||||
.apply { topMargin = 10 }
|
||||
}
|
||||
body.addView(countdownLabel)
|
||||
body.addView(progressBar)
|
||||
|
||||
// ── الأزرار ──
|
||||
val buttonsRow =
|
||||
LinearLayout(ctx).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
layoutParams =
|
||||
LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
.apply { topMargin = 26 }
|
||||
}
|
||||
buttonsRow.addView(
|
||||
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") }
|
||||
}
|
||||
)
|
||||
buttonsRow.addView(
|
||||
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() }
|
||||
}
|
||||
)
|
||||
body.addView(buttonsRow)
|
||||
|
||||
card.tag = mapOf("countdownLabel" to countdownLabel, "progressBar" to progressBar)
|
||||
return card
|
||||
}
|
||||
|
||||
private fun onFoodOrderAccepted() {
|
||||
stopSound()
|
||||
countDownTimer?.cancel()
|
||||
TripOverlayPlugin.notifyFoodOrderAccepted(currentFoodOrderId)
|
||||
bringAppToForeground()
|
||||
removeOverlayView()
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun parseFoodData(json: String): FoodInfo? {
|
||||
return try {
|
||||
val obj = JSONObject(json)
|
||||
FoodInfo(
|
||||
orderId = obj.getString("orderId"),
|
||||
merchantName = obj.optString("merchantName", ""),
|
||||
merchantAddress = obj.optString("merchantAddress", ""),
|
||||
deliveryFeeText = obj.optString("deliveryFeeText", ""),
|
||||
cashToCollectText = obj.optString("cashToCollectText", ""),
|
||||
distanceText = obj.optString("distanceText", ""),
|
||||
itemsCount = obj.optInt("itemsCount", 0),
|
||||
isCash = obj.optBoolean("isCash", false)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "parseFoodData failed: ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
private fun buildOverlayView(trip: TripInfo, autoCloseSeconds: Int): View {
|
||||
val ctx = this
|
||||
|
||||
@@ -502,7 +797,12 @@ class TripOverlayService : Service() {
|
||||
stopSound() // 🔴
|
||||
countDownTimer?.cancel()
|
||||
if (reason == "rejected" || reason == "timeout") {
|
||||
TripOverlayPlugin.notifyTripRejected(currentTripId)
|
||||
// النتيجة تذهب لقناة النوع المعروض — لا نخلط عرض توصيل بعرض رحلة
|
||||
if (isFoodOverlay) {
|
||||
TripOverlayPlugin.notifyFoodOrderRejected(currentFoodOrderId)
|
||||
} else {
|
||||
TripOverlayPlugin.notifyTripRejected(currentTripId)
|
||||
}
|
||||
}
|
||||
removeOverlayView()
|
||||
stopSelf()
|
||||
@@ -516,7 +816,11 @@ class TripOverlayService : Service() {
|
||||
Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or
|
||||
Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||
)
|
||||
putExtra("acceptedTripId", currentTripId)
|
||||
if (isFoodOverlay) {
|
||||
putExtra("acceptedFoodOrderId", currentFoodOrderId)
|
||||
} else {
|
||||
putExtra("acceptedTripId", currentTripId)
|
||||
}
|
||||
}
|
||||
startActivity(launchIntent)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user