From 8ae6a2e2daa99de761f7ea50b5765d2758ffbb2b Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Sat, 4 Jul 2026 15:39:18 +0300 Subject: [PATCH] Update: 2026-07-04 15:39:18 --- .../Services/LocationIntelligenceEngine.php | 18 +- .../android/app/src/main/AndroidManifest.xml | 1 + .../siro_rider/AndroidLiveUpdatePlugin.kt | 311 ++++++++++++++++++ .../com/siro/siro_rider/MainActivity.kt | 12 + .../home/map/ride_lifecycle_controller.dart | 128 +++++-- siro_rider/lib/main.dart | 19 +- .../services/android_live_update_service.dart | 122 +++++++ siro_rider/pubspec.lock | 12 +- 8 files changed, 580 insertions(+), 43 deletions(-) create mode 100644 siro_rider/android/app/src/main/kotlin/com/siro/siro_rider/AndroidLiveUpdatePlugin.kt create mode 100644 siro_rider/lib/services/android_live_update_service.dart 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: