Update: 2026-07-04 15:39:18

This commit is contained in:
Hamza-Ayed
2026-07-04 15:39:18 +03:00
parent c7bd644b88
commit 8ae6a2e2da
8 changed files with 580 additions and 43 deletions
@@ -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)
@@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
@@ -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<String>("rideId") ?: ""
val driverName = call.argument<String>("driverName") ?: ""
val etaText = call.argument<String>("etaText") ?: ""
val progress = call.argument<Double>("progress") ?: 0.0
val carDetails = call.argument<String>("carDetails") ?: ""
startLiveUpdate(rideId, driverName, etaText, progress, carDetails)
result.success(true)
}
"updateProgress" -> {
val progress = call.argument<Double>("progress") ?: 0.0
val etaText = call.argument<String>("etaText") ?: ""
updateProgress(progress, etaText)
result.success(true)
}
"updateStatus" -> {
val status = call.argument<String>("status") ?: ""
val driverName = call.argument<String>("driverName") ?: ""
val etaText = call.argument<String>("etaText") ?: ""
val progress = call.argument<Double>("progress") ?: 0.0
val carDetails = call.argument<String>("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()
}
}
}
}
@@ -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 --------
@@ -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<PaymentController>().isWalletChecked.toString(),
"is_destination_match": box.read(BoxName.isDestinationMatch)?.toString() ?? "0",
"is_destination_match":
box.read(BoxName.isDestinationMatch)?.toString() ?? "0",
"has_steps": Get.find<WayPointController>().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();
}
});
+13 -6
View File
@@ -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<void> 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<void> 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<void> 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();
@@ -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<bool> init() async {
if (!Platform.isAndroid) return false;
try {
final supported = await _channel.invokeMethod<bool>('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<void> 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<void> 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<void> 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<void> 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;
}
}
+6 -6
View File
@@ -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: