diff --git a/backend/core/Services/LocationIntelligenceEngine.php b/backend/core/Services/LocationIntelligenceEngine.php
index bebf1251..17f3aa36 100644
--- a/backend/core/Services/LocationIntelligenceEngine.php
+++ b/backend/core/Services/LocationIntelligenceEngine.php
@@ -97,17 +97,27 @@ class LocationIntelligenceEngine {
private function sendCampaignNotification($passengerId, $zone) {
// Get passenger token
- $sql = "SELECT users_token, country_code FROM users WHERE users_id = :pid";
+ $sql = "SELECT t.token as users_token, p.country_code
+ FROM passengers p
+ JOIN tokens t ON p.id = t.passengerID
+ WHERE p.id = :pid";
$stmt = $this->db->prepare($sql);
$stmt->execute([':pid' => $passengerId]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && !empty($user['users_token'])) {
- $title = "مرحباً بك في " . $zone['zone_name'] . " \u{1F389}";
+ $title = "مرحباً بك في " . $zone['zone_name'] . " 🎉";
$body = "اطلب رحلتك الآن من " . $zone['zone_name'] . " واستمتع بتجربة سيرو!";
- // Send Push
- sendFCM_Internal([$user['users_token']], $title, $body, ['type' => 'geofence_promo'], '');
+ // Decrypt token
+ require_once __DIR__ . '/../Security/EncryptionHelper.php';
+ $encryptionHelper = new EncryptionHelper();
+ $decryptedToken = $encryptionHelper->decryptData($user['users_token']);
+
+ if ($decryptedToken) {
+ // Send Push
+ sendFCM_Internal($decryptedToken, $title, $body, ['type' => 'geofence_promo'], 'Marketing');
+ }
// Log it
$logSql = "INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by)
diff --git a/siro_rider/android/app/src/main/AndroidManifest.xml b/siro_rider/android/app/src/main/AndroidManifest.xml
index 2e56c4db..5379c31c 100644
--- a/siro_rider/android/app/src/main/AndroidManifest.xml
+++ b/siro_rider/android/app/src/main/AndroidManifest.xml
@@ -18,6 +18,7 @@
+
diff --git a/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/AndroidLiveUpdatePlugin.kt b/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/AndroidLiveUpdatePlugin.kt
new file mode 100644
index 00000000..c8102629
--- /dev/null
+++ b/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/AndroidLiveUpdatePlugin.kt
@@ -0,0 +1,311 @@
+package com.siro.siro_rider
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.BitmapFactory
+import android.os.Build
+import androidx.core.app.NotificationCompat
+import io.flutter.plugin.common.MethodChannel
+
+/**
+ * Android 16 Progress Style Live Update Plugin
+ *
+ * يستخدم NotificationCompat مع setProgressStyle لتوفير
+ * تجربة Live Update متقدمة على Android 16 (API 36) والأجهزة الأحدث.
+ *
+ * الميزات:
+ * - شريط تقدم متحرك (Progress Style)
+ * - تحديث مباشر للوقت المتبقي
+ * - دعم الحالات المختلفة (بحث، في الطريق، وصل، رحلة جارية)
+ * - إشعار ثابت (Ongoing) لا يمكن إلغاؤه أثناء الرحلة
+ */
+class AndroidLiveUpdatePlugin(private val context: Context) {
+
+ companion object {
+ private const val CHANNEL_ID = "siro_live_ride_progress"
+ private const val CHANNEL_NAME = "Siro Live Ride Progress"
+ private const val CHANNEL_DESC = "Live progress updates for your current Siro ride"
+ private const val NOTIFICATION_ID = 999
+ private const val SUMMARY_NOTIFICATION_ID = 1000
+
+ // Ride status constants
+ private const val STATUS_SEARCHING = "searching"
+ private const val STATUS_DRIVER_ON_WAY = "driver_on_way"
+ private const val STATUS_DRIVER_ARRIVED = "driver_arrived"
+ private const val STATUS_IN_PROGRESS = "in_progress"
+ private const val STATUS_FINISHED = "finished"
+ }
+
+ private val notificationManager: NotificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ private var currentProgress: Int = 0
+ private var currentMaxProgress: Int = 100
+ private var currentStatus: String = STATUS_SEARCHING
+ private var currentTitle: String = ""
+ private var currentBody: String = ""
+
+ init {
+ createNotificationChannel()
+ }
+
+ /**
+ * إنشاء قناة الإشعارات المخصصة للرحلة
+ * مع دعم Android 16 Progress Style
+ */
+ private fun createNotificationChannel() {
+ val channel = NotificationChannel(
+ CHANNEL_ID,
+ CHANNEL_NAME,
+ NotificationManager.IMPORTANCE_LOW // Low importance for progress style
+ ).apply {
+ description = CHANNEL_DESC
+ setShowBadge(false)
+ enableVibration(false)
+ enableLights(false)
+ // Android 16: دعم التقدم المستمر
+ if (Build.VERSION.SDK_INT >= 36) {
+ // يمكن إضافة خصائص Android 16 هنا
+ }
+ }
+ notificationManager.createNotificationChannel(channel)
+ }
+
+ /**
+ * بدء التحديث المباشر للرحلة
+ * يعرض إشعاراً ثابتاً مع شريط تقدم
+ */
+ fun startLiveUpdate(
+ rideId: String,
+ driverName: String,
+ etaText: String,
+ progress: Double,
+ carDetails: String
+ ) {
+ currentStatus = STATUS_DRIVER_ON_WAY
+ currentTitle = "🚗 السائق في الطريق إليك"
+ currentBody = "$driverName • $carDetails • $etaText"
+ currentProgress = (progress * 100).toInt().coerceIn(0, 100)
+ currentMaxProgress = 100
+
+ showNotification(
+ title = currentTitle,
+ body = currentBody,
+ progress = currentProgress,
+ maxProgress = currentMaxProgress,
+ indeterminate = false,
+ ongoing = true
+ )
+ }
+
+ /**
+ * تحديث التقدم والوقت المتبقي
+ * يستخدم Progress Style مع animate للتحديثات
+ */
+ fun updateProgress(progress: Double, etaText: String) {
+ currentProgress = (progress * 100).toInt().coerceIn(0, 100)
+ currentBody = when (currentStatus) {
+ STATUS_DRIVER_ON_WAY -> {
+ val parts = currentBody.split(" • ")
+ if (parts.size >= 3) {
+ "${parts[0]} • ${parts[1]} • $etaText"
+ } else {
+ "$currentBody • $etaText"
+ }
+ }
+ STATUS_IN_PROGRESS -> "المتبقي تقريبًا: $etaText"
+ else -> currentBody
+ }
+
+ showNotification(
+ title = currentTitle,
+ body = currentBody,
+ progress = currentProgress,
+ maxProgress = currentMaxProgress,
+ indeterminate = false,
+ ongoing = true
+ )
+ }
+
+ /**
+ * تحديث حالة الرحلة بالكامل
+ * يستخدم لتغيير الحالة بين (في الطريق، وصل، رحلة جارية)
+ */
+ fun updateStatus(
+ status: String,
+ driverName: String,
+ etaText: String,
+ progress: Double,
+ carDetails: String
+ ) {
+ currentStatus = status
+ currentProgress = (progress * 100).toInt().coerceIn(0, 100)
+
+ when (status) {
+ STATUS_SEARCHING -> {
+ currentTitle = "🔍 جاري البحث عن سائق"
+ currentBody = etaText
+ showNotification(
+ title = currentTitle,
+ body = currentBody,
+ progress = 0,
+ maxProgress = 0,
+ indeterminate = true,
+ ongoing = true
+ )
+ }
+ STATUS_DRIVER_ON_WAY -> {
+ currentTitle = "🚗 السائق في الطريق إليك"
+ currentBody = "$driverName • $carDetails • $etaText"
+ showNotification(
+ title = currentTitle,
+ body = currentBody,
+ progress = currentProgress,
+ maxProgress = 100,
+ indeterminate = false,
+ ongoing = true
+ )
+ }
+ STATUS_DRIVER_ARRIVED -> {
+ currentTitle = "📍 السائق وصل"
+ currentBody = "الرجاء التوجّه لمقابلة $driverName عند نقطة الالتقاء"
+ showNotification(
+ title = currentTitle,
+ body = currentBody,
+ progress = 100,
+ maxProgress = 100,
+ indeterminate = false,
+ ongoing = true
+ )
+ }
+ STATUS_IN_PROGRESS -> {
+ currentTitle = "🚀 الرحلة جارية الآن"
+ currentBody = "المتبقي تقريبًا: $etaText"
+ showNotification(
+ title = currentTitle,
+ body = currentBody,
+ progress = currentProgress,
+ maxProgress = 100,
+ indeterminate = false,
+ ongoing = true
+ )
+ }
+ }
+ }
+
+ /**
+ * إنهاء التحديث المباشر وإزالة الإشعار
+ */
+ fun endLiveUpdate() {
+ notificationManager.cancel(NOTIFICATION_ID)
+ currentStatus = STATUS_FINISHED
+ currentProgress = 0
+ }
+
+ /**
+ * إظهار الإشعار مع الإعدادات المناسبة
+ */
+ private fun showNotification(
+ title: String,
+ body: String,
+ progress: Int,
+ maxProgress: Int,
+ indeterminate: Boolean,
+ ongoing: Boolean
+ ) {
+ // Intent لفتح التطبيق عند النقر على الإشعار
+ val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.apply {
+ flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
+ putExtra("notification_ride", true)
+ }
+
+ val pendingIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val notificationBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(android.R.drawable.ic_dialog_info)
+ .setLargeIcon(
+ BitmapFactory.decodeResource(
+ context.resources,
+ android.R.drawable.ic_menu_directions
+ )
+ )
+ .setContentTitle(title)
+ .setContentText(body)
+ .setStyle(
+ NotificationCompat.BigTextStyle()
+ .bigText(body)
+ )
+ .setPriority(NotificationCompat.PRIORITY_LOW)
+ .setOngoing(ongoing)
+ .setAutoCancel(false)
+ .setOnlyAlertOnce(true)
+ .setContentIntent(pendingIntent)
+ .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+
+ // تطبيق Progress Style
+ if (indeterminate) {
+ notificationBuilder.setProgress(0, 0, true)
+ } else {
+ notificationBuilder.setProgress(maxProgress, progress, false)
+ }
+
+ // Android 16: إضافة إجراءات سريعة للإشعار
+ if (Build.VERSION.SDK_INT >= 36) {
+ // يمكن إضافة إجراءات Android 16 هنا
+ // مثل: إظهار الخريطة، مشاركة الرحلة، إلخ
+ }
+
+ notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
+ }
+
+ /**
+ * معالجة استدعاءات MethodChannel
+ */
+ fun handleMethodCall(call: MethodChannel.MethodCall, result: MethodChannel.Result) {
+ when (call.method) {
+ "startLiveUpdate" -> {
+ val rideId = call.argument("rideId") ?: ""
+ val driverName = call.argument("driverName") ?: ""
+ val etaText = call.argument("etaText") ?: ""
+ val progress = call.argument("progress") ?: 0.0
+ val carDetails = call.argument("carDetails") ?: ""
+ startLiveUpdate(rideId, driverName, etaText, progress, carDetails)
+ result.success(true)
+ }
+ "updateProgress" -> {
+ val progress = call.argument("progress") ?: 0.0
+ val etaText = call.argument("etaText") ?: ""
+ updateProgress(progress, etaText)
+ result.success(true)
+ }
+ "updateStatus" -> {
+ val status = call.argument("status") ?: ""
+ val driverName = call.argument("driverName") ?: ""
+ val etaText = call.argument("etaText") ?: ""
+ val progress = call.argument("progress") ?: 0.0
+ val carDetails = call.argument("carDetails") ?: ""
+ updateStatus(status, driverName, etaText, progress, carDetails)
+ result.success(true)
+ }
+ "endLiveUpdate" -> {
+ endLiveUpdate()
+ result.success(true)
+ }
+ "isSupported" -> {
+ result.success(Build.VERSION.SDK_INT >= 30) // يدعم Android 11+
+ }
+ else -> {
+ result.notImplemented()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/MainActivity.kt b/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/MainActivity.kt
index 28264611..27fe7165 100644
--- a/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/MainActivity.kt
+++ b/siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/MainActivity.kt
@@ -29,6 +29,10 @@ class MainActivity : FlutterFragmentActivity() {
private val PIP_CHANNEL = "siro/pip"
private var pipEnabled = false // هل الرحلة نشطة ويجب تفعيل PiP عند الخروج؟
+ // قناة Android Live Update (Android 16 Progress Style)
+ private val LIVE_UPDATE_CHANNEL = "siro/android_live_update"
+ private lateinit var liveUpdatePlugin: AndroidLiveUpdatePlugin
+
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -72,6 +76,14 @@ class MainActivity : FlutterFragmentActivity() {
}
}
}
+
+ // -------- 3) قناة Android Live Update (Progress Style) --------
+ liveUpdatePlugin = AndroidLiveUpdatePlugin(this)
+
+ MethodChannel(flutterEngine.dartExecutor.binaryMessenger, LIVE_UPDATE_CHANNEL)
+ .setMethodCallHandler { call, result ->
+ liveUpdatePlugin.handleMethodCall(call, result)
+ }
}
// -------- PiP Helper Methods --------
diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart
index 7a24cb24..52e9a612 100644
--- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart
+++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart
@@ -45,6 +45,7 @@ import 'ui_interactions_controller.dart';
import 'map_socket_controller.dart';
import '../decode_polyline_isolate.dart';
import '../ios_live_activity_service.dart';
+import '../../../services/android_live_update_service.dart';
import '../../firebase/local_notification.dart';
import '../../firebase/notification_service.dart';
import '../../functions/audio_record1.dart';
@@ -737,8 +738,10 @@ class RideLifecycleController extends GetxController {
void _showAiNegotiatorDialog() {
final double currentPrice = double.tryParse(totalPassenger) ?? 0;
final double suggestedPrice = currentPrice * 1.05; // 5% AI suggestion
- final String currency = box.read(BoxName.serverChosen)?.toString().contains('Syria') == true
- ? 'ل.س' : 'SAR';
+ final String currency =
+ box.read(BoxName.serverChosen)?.toString().contains('Syria') == true
+ ? 'ل.س'
+ : 'SAR';
Get.dialog(
Dialog(
@@ -757,24 +760,30 @@ class RideLifecycleController extends GetxController {
borderRadius: BorderRadius.circular(50),
border: Border.all(color: const Color(0xFF0F3460), width: 2),
),
- child: const Icon(Icons.psychology_rounded, color: Color(0xFF4FC3F7), size: 36),
+ child: const Icon(Icons.psychology_rounded,
+ color: Color(0xFF4FC3F7), size: 36),
),
const SizedBox(height: 16),
Text(
'🤖 مقترح الذكاء الاصطناعي'.tr,
- style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold),
+ style: const TextStyle(
+ color: Colors.white,
+ fontSize: 18,
+ fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Text(
- 'لم يقبل أي سائق طلبك بالسعر الحالي. يقترح AI رفع السعر قليلاً لتسريع القبول.'.tr,
+ 'لم يقبل أي سائق طلبك بالسعر الحالي. يقترح AI رفع السعر قليلاً لتسريع القبول.'
+ .tr,
style: const TextStyle(color: Color(0xFFB0BEC5), fontSize: 13),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
// Price comparison
Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ padding:
+ const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: const Color(0xFF0F3460),
borderRadius: BorderRadius.circular(12),
@@ -784,17 +793,28 @@ class RideLifecycleController extends GetxController {
children: [
Column(
children: [
- Text('السعر الحالي'.tr, style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 11)),
+ Text('السعر الحالي'.tr,
+ style: const TextStyle(
+ color: Color(0xFF90A4AE), fontSize: 11)),
Text('${currentPrice.toStringAsFixed(0)} $currency',
- style: const TextStyle(color: Colors.white70, fontSize: 15, decoration: TextDecoration.lineThrough)),
+ style: const TextStyle(
+ color: Colors.white70,
+ fontSize: 15,
+ decoration: TextDecoration.lineThrough)),
],
),
- const Icon(Icons.arrow_forward_rounded, color: Color(0xFF4FC3F7)),
+ const Icon(Icons.arrow_forward_rounded,
+ color: Color(0xFF4FC3F7)),
Column(
children: [
- Text('السعر المقترح'.tr, style: const TextStyle(color: Color(0xFF4FC3F7), fontSize: 11)),
+ Text('السعر المقترح'.tr,
+ style: const TextStyle(
+ color: Color(0xFF4FC3F7), fontSize: 11)),
Text('${suggestedPrice.toStringAsFixed(0)} $currency',
- style: const TextStyle(color: Color(0xFF4FC3F7), fontSize: 17, fontWeight: FontWeight.bold)),
+ style: const TextStyle(
+ color: Color(0xFF4FC3F7),
+ fontSize: 17,
+ fontWeight: FontWeight.bold)),
],
),
],
@@ -811,9 +831,11 @@ class RideLifecycleController extends GetxController {
},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10)),
),
- child: Text('إلغاء'.tr, style: const TextStyle(color: Colors.red)),
+ child: Text('إلغاء'.tr,
+ style: const TextStyle(color: Colors.red)),
),
),
const SizedBox(width: 10),
@@ -825,9 +847,13 @@ class RideLifecycleController extends GetxController {
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4FC3F7),
- shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10)),
),
- child: Text('قبول +5%'.tr, style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold)),
+ child: Text('قبول +5%'.tr,
+ style: const TextStyle(
+ color: Colors.black,
+ fontWeight: FontWeight.bold)),
),
),
],
@@ -846,12 +872,14 @@ class RideLifecycleController extends GetxController {
update();
// تحديث قاعدة البيانات بالسعر الجديد
- await CRUD().post(link: "${AppLink.server}/ride/rides/update.php", payload: {
+ await CRUD()
+ .post(link: "${AppLink.server}/ride/rides/update.php", payload: {
"id": rideId,
"price": newPrice.toStringAsFixed(2),
});
- Log.print('[AI Negotiator Phase 2] Price accepted by passenger: $newPrice — restarting search...');
+ Log.print(
+ '[AI Negotiator Phase 2] Price accepted by passenger: $newPrice — restarting search...');
notifiedDrivers.clear();
// إعادة البحث مع السعر الجديد (Phase 2 retry)
@@ -1016,6 +1044,12 @@ class RideLifecycleController extends GetxController {
etaText: stringRemainingTimeToPassenger,
progress: currentProgress.clamp(0.0, 1.0),
);
+
+ // Android Live Update: تحديث التقدم أثناء انتظار السائق
+ AndroidLiveUpdateService.updateProgress(
+ progress: currentProgress.clamp(0.0, 1.0),
+ etaText: stringRemainingTimeToPassenger,
+ );
}
if (_driverEtaCountdownTicks % beginRideInterval == 0) {
@@ -1149,6 +1183,12 @@ class RideLifecycleController extends GetxController {
etaText: stringRemainingTimeRideBegin,
progress: progressTimerRideBegin.clamp(0.0, 1.0),
);
+
+ // Android Live Update: تحديث التقدم أثناء الرحلة
+ AndroidLiveUpdateService.updateProgress(
+ progress: progressTimerRideBegin.clamp(0.0, 1.0),
+ etaText: stringRemainingTimeRideBegin,
+ );
}
if (remainingSeconds % 60 == 0 || remainingSeconds == 0) {
@@ -2093,7 +2133,8 @@ class RideLifecycleController extends GetxController {
// 🆕 Save is_destination_match so retry_search_drivers can pass it forward
final int isMatchFlag = (res['is_destination_match'] ?? 0) is int
? res['is_destination_match'] ?? 0
- : int.tryParse(res['is_destination_match']?.toString() ?? '0') ?? 0;
+ : int.tryParse(res['is_destination_match']?.toString() ?? '0') ??
+ 0;
box.write(BoxName.isDestinationMatch, isMatchFlag.toString());
totalPassenger = totalPassengerSpeed;
@@ -2135,12 +2176,15 @@ class RideLifecycleController extends GetxController {
if (res is Map && res['status'] == 'success') {
hasCompetitorData = res['has_competitor_data'] == true;
- competitorAvgPrice = double.tryParse(res['competitor_avg_price']?.toString() ?? '');
- savingsPercent = double.tryParse(res['savings_percent']?.toString() ?? '');
+ competitorAvgPrice =
+ double.tryParse(res['competitor_avg_price']?.toString() ?? '');
+ savingsPercent =
+ double.tryParse(res['savings_percent']?.toString() ?? '');
savingsLabel = res['savings_label']?.toString();
topCompetitor = res['top_competitor']?.toString();
driverExtraLabel = res['driver_extra_label']?.toString();
- driverExtraAmount = double.tryParse(res['driver_extra_amount']?.toString() ?? '');
+ driverExtraAmount =
+ double.tryParse(res['driver_extra_amount']?.toString() ?? '');
update();
}
} catch (e) {
@@ -2912,6 +2956,15 @@ class RideLifecycleController extends GetxController {
statusRide = 'Arrived';
await RideLiveNotification.showDriverArrived(driverName);
+ // Android Live Update: تحديث الحالة إلى "وصل السائق"
+ await AndroidLiveUpdateService.updateStatus(
+ status: 'driver_arrived',
+ driverName: driverName,
+ etaText: 'لقد وصل السائق',
+ progress: 1.0,
+ carDetails: '$make • $model • $carColor',
+ );
+
uiInteractions.driverArrivePassengerDialoge();
startTimerDriverWaitPassenger5Minute();
@@ -2957,7 +3010,9 @@ class RideLifecycleController extends GetxController {
"Please make sure not to leave any personal belongings in the car.".tr,
'tone1',
);
+ // End both iOS and Android Live Updates
IosLiveActivityService.endRideActivity();
+ await AndroidLiveUpdateService.endLiveUpdate();
PipService.disablePip();
await RideLiveNotification.cancel();
@@ -2980,6 +3035,7 @@ class RideLifecycleController extends GetxController {
if (Get.isDialogOpen == true) Get.back();
await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity();
+ await AndroidLiveUpdateService.endLiveUpdate();
PipService.disablePip();
Get.defaultDialog(
@@ -3685,7 +3741,8 @@ class RideLifecycleController extends GetxController {
for (final m in newMarkers) {
await ctrl.addMarker(m);
}
- Log.print('✅ Added ${newMarkers.length} markers via controller.addMarker()');
+ Log.print(
+ '✅ Added ${newMarkers.length} markers via controller.addMarker()');
} else {
Log.print('⚠️ mapController is null, relying on declarative markers');
}
@@ -3792,6 +3849,7 @@ class RideLifecycleController extends GetxController {
await getUpdatedRideForDriverApply(rideId);
}
+ // Start iOS Live Activity
await IosLiveActivityService.startRideActivity(
rideId: rideId,
driverName: driverName,
@@ -3800,6 +3858,15 @@ class RideLifecycleController extends GetxController {
progress: 0.0,
);
+ // Start Android Live Update (Progress Style)
+ await AndroidLiveUpdateService.startLiveUpdate(
+ rideId: rideId,
+ driverName: driverName,
+ etaText: stringRemainingTimeToPassenger,
+ progress: 0.0,
+ carDetails: '$make • $model • $carColor',
+ );
+
_showRideStartNotifications();
final etaText = stringRemainingTimeToPassenger;
final carInfo = '$make • $model • $licensePlate';
@@ -3918,6 +3985,7 @@ class RideLifecycleController extends GetxController {
currentRideState.value = RideState.cancelled;
await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity();
+ await AndroidLiveUpdateService.endLiveUpdate();
PipService.disablePip();
if (rideId != 'yet' && rideId != null) {
@@ -4033,6 +4101,7 @@ class RideLifecycleController extends GetxController {
stopAllTimers();
await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity();
+ await AndroidLiveUpdateService.endLiveUpdate();
PipService.disablePip();
_isCancelProcessed = false;
currentRideState.value = RideState.noRide;
@@ -4066,7 +4135,8 @@ class RideLifecycleController extends GetxController {
try {
final bool isPhase2 = newPrice != null && newPrice > 0;
- Log.print("🔄 ${isPhase2 ? '[Phase 2]' : '[Phase 1]'} Retrying search for ride ID: $rideId");
+ Log.print(
+ "🔄 ${isPhase2 ? '[Phase 2]' : '[Phase 1]'} Retrying search for ride ID: $rideId");
var payload = {
"ride_id": rideId.toString(),
@@ -4090,7 +4160,8 @@ class RideLifecycleController extends GetxController {
"price_for_driver": costForDriver.toString(),
"car_type": box.read(BoxName.carType).toString(),
"is_wallet": Get.find().isWalletChecked.toString(),
- "is_destination_match": box.read(BoxName.isDestinationMatch)?.toString() ?? "0",
+ "is_destination_match":
+ box.read(BoxName.isDestinationMatch)?.toString() ?? "0",
"has_steps": Get.find().wayPoints.length > 1
? "true"
: "false",
@@ -4105,7 +4176,8 @@ class RideLifecycleController extends GetxController {
if (response['status'] == 'success') {
Log.print("✅ Search reset successfully.");
- if (!isPhase2) startSearchingTimer(); // Phase 1 يعيد العداد، Phase 2 يُدار من increasePriceAndRestartSearch
+ if (!isPhase2)
+ startSearchingTimer(); // Phase 1 يعيد العداد، Phase 2 يُدار من increasePriceAndRestartSearch
} else {
Log.print("❌ Failed to reset search: $response");
handleNoDriverFound();
@@ -4139,7 +4211,8 @@ class RideLifecycleController extends GetxController {
// ─────────────────────────────────────────────────────────────
if (seconds == 20 && !_aiSilentRetryFired) {
_aiSilentRetryFired = true;
- Log.print("🤖 [AI Negotiator Phase 1] 20s — silent retry with driver bonus...");
+ Log.print(
+ "🤖 [AI Negotiator Phase 1] 20s — silent retry with driver bonus...");
retrySearchForDrivers();
return;
}
@@ -4151,7 +4224,8 @@ class RideLifecycleController extends GetxController {
// ─────────────────────────────────────────────────────────────
if (seconds >= 45) {
timer.cancel();
- Log.print("🤖 [AI Negotiator Phase 2] 45s — showing AI price suggestion dialog...");
+ Log.print(
+ "🤖 [AI Negotiator Phase 2] 45s — showing AI price suggestion dialog...");
_showAiNegotiatorDialog();
}
});
diff --git a/siro_rider/lib/main.dart b/siro_rider/lib/main.dart
index a7f31b90..f72a2d46 100644
--- a/siro_rider/lib/main.dart
+++ b/siro_rider/lib/main.dart
@@ -20,6 +20,7 @@ import 'package:wakelock_plus/wakelock_plus.dart';
import 'constant/info.dart';
import 'constant/box_name.dart';
import 'controller/home/ios_live_activity_service.dart';
+import 'services/android_live_update_service.dart';
import 'controller/local/local_controller.dart';
import 'controller/local/translations.dart';
import 'controller/themes/themes.dart';
@@ -39,7 +40,7 @@ DbSql sql = DbSql.instance;
Future backgroundMessageHandler(RemoteMessage message) async {
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
Log.print("Handling a background message: ${message.messageId}");
-
+
if (message.data.isNotEmpty) {
try {
await GetStorage.init();
@@ -51,10 +52,12 @@ Future backgroundMessageHandler(RemoteMessage message) async {
if (!serviceEnabled) return;
LocationPermission permission = await Geolocator.checkPermission();
- if (permission == LocationPermission.denied || permission == LocationPermission.deniedForever) return;
+ if (permission == LocationPermission.denied ||
+ permission == LocationPermission.deniedForever) return;
+
+ final position = await Geolocator.getCurrentPosition(
+ desiredAccuracy: LocationAccuracy.high);
- final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
-
final url = Uri.parse('${AppLink.server}/api/location/sync_location.php');
await http.post(url, body: {
'passenger_id': passengerId.toString(),
@@ -62,9 +65,10 @@ Future backgroundMessageHandler(RemoteMessage message) async {
'lng': position.longitude.toString(),
'source': 'silent_push',
});
-
+
// Update geofences on device silently
- await SiroGeofencingService.syncZonesWithServer(position.latitude, position.longitude);
+ await SiroGeofencingService.syncZonesWithServer(
+ position.latitude, position.longitude);
} catch (e) {
Log.print("Silent push location update failed: $e");
}
@@ -89,6 +93,9 @@ void main() {
// ✅ التعديل هنا: تهيئة خدمة الـ Live Activity للآيفون
IosLiveActivityService.init();
+ // ✅ تهيئة خدمة Android Live Update (Progress Style)
+ await AndroidLiveUpdateService.init();
+
// Initialize Geofencing Service
if (Platform.isAndroid || Platform.isIOS) {
await SiroGeofencingService.initAndStart();
diff --git a/siro_rider/lib/services/android_live_update_service.dart b/siro_rider/lib/services/android_live_update_service.dart
new file mode 100644
index 00000000..5ea89b5c
--- /dev/null
+++ b/siro_rider/lib/services/android_live_update_service.dart
@@ -0,0 +1,122 @@
+import 'dart:io';
+import 'package:flutter/services.dart';
+import '../print.dart';
+
+/// خدمة Android Live Update (Progress Style)
+///
+/// توفر واجهة موحدة للتحديث المباشر لحالة الرحلة على Android
+/// باستخدام NotificationCompat مع Progress Style (Android 16+)
+///
+/// هذه الخدمة مشابهة لـ [IosLiveActivityService] ولكن لمنصة Android
+class AndroidLiveUpdateService {
+ static const MethodChannel _channel =
+ MethodChannel('siro/android_live_update');
+
+ static bool _isSupported = false;
+ static bool _isActive = false;
+
+ /// التحقق من دعم الجهاز للخدمة
+ /// تعود true إذا كان Android 11+ (API 30+)
+ static Future init() async {
+ if (!Platform.isAndroid) return false;
+
+ try {
+ final supported = await _channel.invokeMethod('isSupported');
+ _isSupported = supported ?? false;
+ Log.print(
+ '📱 AndroidLiveUpdateService initialized: supported=$_isSupported');
+ return _isSupported;
+ } catch (e) {
+ Log.print('❌ AndroidLiveUpdateService init error: $e');
+ _isSupported = false;
+ return false;
+ }
+ }
+
+ /// بدء التحديث المباشر للرحلة (عند قبول السائق)
+ static Future startLiveUpdate({
+ required String rideId,
+ required String driverName,
+ required String etaText,
+ required double progress,
+ required String carDetails,
+ }) async {
+ if (!Platform.isAndroid || !_isSupported) return;
+
+ try {
+ await _channel.invokeMethod('startLiveUpdate', {
+ 'rideId': rideId,
+ 'driverName': driverName,
+ 'etaText': etaText,
+ 'progress': progress,
+ 'carDetails': carDetails,
+ });
+ _isActive = true;
+ Log.print('✅ Android Live Update Started');
+ } catch (e) {
+ Log.print('❌ AndroidLiveUpdateService startLiveUpdate error: $e');
+ }
+ }
+
+ /// تحديث التقدم والوقت المتبقي
+ static Future updateProgress({
+ required double progress,
+ required String etaText,
+ }) async {
+ if (!Platform.isAndroid || !_isSupported || !_isActive) return;
+
+ try {
+ await _channel.invokeMethod('updateProgress', {
+ 'progress': progress,
+ 'etaText': etaText,
+ });
+ } catch (e) {
+ Log.print('❌ AndroidLiveUpdateService updateProgress error: $e');
+ }
+ }
+
+ /// تحديث حالة الرحلة بالكامل
+ static Future updateStatus({
+ required String status,
+ required String driverName,
+ required String etaText,
+ required double progress,
+ String carDetails = '',
+ }) async {
+ if (!Platform.isAndroid || !_isSupported || !_isActive) return;
+
+ try {
+ await _channel.invokeMethod('updateStatus', {
+ 'status': status,
+ 'driverName': driverName,
+ 'etaText': etaText,
+ 'progress': progress,
+ 'carDetails': carDetails,
+ });
+ Log.print('✅ Android Live Update Status: $status');
+ } catch (e) {
+ Log.print('❌ AndroidLiveUpdateService updateStatus error: $e');
+ }
+ }
+
+ /// إنهاء التحديث المباشر وإزالة الإشعار
+ static Future endLiveUpdate() async {
+ if (!Platform.isAndroid || !_isSupported || !_isActive) return;
+
+ try {
+ await _channel.invokeMethod('endLiveUpdate');
+ _isActive = false;
+ Log.print('✅ Android Live Update Ended');
+ } catch (e) {
+ Log.print('❌ AndroidLiveUpdateService endLiveUpdate error: $e');
+ }
+ }
+
+ /// التحقق مما إذا كان التحديث نشطاً
+ static bool get isActive => _isActive;
+
+ /// إعادة تعيين الحالة
+ static void reset() {
+ _isActive = false;
+ }
+}
diff --git a/siro_rider/pubspec.lock b/siro_rider/pubspec.lock
index 0de912cf..2f1e1ab2 100644
--- a/siro_rider/pubspec.lock
+++ b/siro_rider/pubspec.lock
@@ -1297,10 +1297,10 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev"
source: hosted
- version: "0.12.19"
+ version: "0.12.18"
material_color_utilities:
dependency: transitive
description:
@@ -1313,10 +1313,10 @@ packages:
dependency: transitive
description:
name: meta
- sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
- version: "1.18.0"
+ version: "1.17.0"
mime:
dependency: "direct main"
description:
@@ -1885,10 +1885,10 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
+ sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev"
source: hosted
- version: "0.7.11"
+ version: "0.7.9"
timezone:
dependency: transitive
description: