2026-04-03-maplibra come next

This commit is contained in:
Hamza-Ayed
2026-04-03 16:23:14 +03:00
parent c6b27d06d4
commit e325405dff
13 changed files with 363 additions and 830 deletions

View File

@@ -30,6 +30,7 @@ android {
version "3.22.1" version "3.22.1"
} }
} }
kotlinOptions { kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8 jvmTarget = JavaVersion.VERSION_1_8
@@ -47,8 +48,8 @@ android {
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdkVersion = 24 minSdkVersion = 24
targetSdk = 36 targetSdk = 36
versionCode = 60 versionCode = 63
versionName = '1.1.60' versionName = '1.1.63'
multiDexEnabled = true multiDexEnabled = true
ndk { ndk {
abiFilters "armeabi-v7a", "arm64-v8a" abiFilters "armeabi-v7a", "arm64-v8a"

View File

@@ -1,7 +1,6 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.Intaleq.intaleq"> package="com.Intaleq.intaleq">
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
@@ -14,9 +13,6 @@
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" /> <uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-feature android:name="android.hardware.camera" /> <uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" /> <uses-feature android:name="android.hardware.camera.autofocus" />
@@ -30,7 +26,6 @@
android:usesCleartextTraffic="false" android:usesCleartextTraffic="false"
android:networkSecurityConfig="@xml/network_security_config"> android:networkSecurityConfig="@xml/network_security_config">
<!-- ✅ مهم جداً: تعريف أن المشروع يستخدم V2 Embedding -->
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
@@ -39,49 +34,37 @@
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:supportsPictureInPicture="true"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<!-- Flutter -->
<meta-data <meta-data
android:name="io.flutter.embedding.android.NormalTheme" android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" /> android:resource="@style/NormalTheme" />
<!-- Launcher -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- 🔗 App Links -->
<intent-filter android:autoVerify="true"> <intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="intaleqapp.com" android:pathPrefix="/" />
<data <data android:scheme="https" android:host="www.intaleqapp.com"
android:scheme="https"
android:host="intaleqapp.com"
android:pathPrefix="/" />
<data
android:scheme="https"
android:host="www.intaleqapp.com"
android:pathPrefix="/" /> android:pathPrefix="/" />
</intent-filter> </intent-filter>
<!-- 🔗 Custom Scheme -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="intaleq" /> <data android:scheme="intaleq" />
</intent-filter> </intent-filter>
<!-- 🔗 Intercept Geo URIs (geo:lat,lng) -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@@ -89,34 +72,16 @@
<data android:scheme="geo" /> <data android:scheme="geo" />
</intent-filter> </intent-filter>
<!-- 🔗 Intercept External Map URLs (Google/Apple) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" android:host="maps.google.com" />
<data android:scheme="https" android:host="maps.google.com" />
<data android:scheme="https" android:host="maps.apple.com" />
<data android:scheme="https" android:host="goo.gl" />
<data android:scheme="http" android:host="goo.gl" />
<data android:scheme="https" android:host="maps.app.goo.gl" />
<data android:scheme="http" android:host="maps.app.goo.gl" />
</intent-filter>
</activity> </activity>
<!-- Google Maps API -->
<meta-data <meta-data
android:name="com.google.android.geo.API_KEY" android:name="com.google.android.geo.API_KEY"
android:value="${mapsApiKey}" /> android:value="${mapsApiKey}" />
<!-- Firebase Notification Channel -->
<meta-data <meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id" android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" /> android:value="@string/default_notification_channel_id" />
<!-- Local Notifications Receivers -->
<receiver <receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
android:exported="false" /> android:exported="false" />
@@ -129,12 +94,7 @@
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" /> <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<service
android:name=".RideTrackingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
<!-- UCrop -->
<activity <activity
android:name="com.yalantis.ucrop.UCropActivity" android:name="com.yalantis.ucrop.UCropActivity"
android:screenOrientation="portrait" android:screenOrientation="portrait"

View File

@@ -1,8 +1,12 @@
package com.Intaleq.intaleq package com.Intaleq.intaleq
import android.app.AlertDialog import android.app.AlertDialog
import android.app.PictureInPictureParams
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import android.util.Rational
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import android.widget.TextView import android.widget.TextView
@@ -21,8 +25,9 @@ class MainActivity : FlutterFragmentActivity() {
private val SECURITY_CHANNEL_NAME = "com.Intaleq.intaleq/security" private val SECURITY_CHANNEL_NAME = "com.Intaleq.intaleq/security"
private lateinit var securityChannel: MethodChannel private lateinit var securityChannel: MethodChannel
// قناة تتبّع الرحلة (Live Activity على أندرويد) // قناة PiP الجديدة
private val RIDE_TRACKING_CHANNEL = "intaleq/ride_tracking" private val PIP_CHANNEL = "intaleq/pip"
private var pipEnabled = false // هل الرحلة نشطة ويجب تفعيل PiP عند الخروج؟
override fun configureFlutterEngine(flutterEngine: FlutterEngine) { override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine) super.configureFlutterEngine(flutterEngine)
@@ -43,50 +48,24 @@ class MainActivity : FlutterFragmentActivity() {
} }
} }
// -------- 2) قناة تتبع الرحلة (Ride Tracking) -------- // -------- 2) قناة PiP (Picture-in-Picture) --------
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, RIDE_TRACKING_CHANNEL) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL)
.setMethodCallHandler { call, result -> .setMethodCallHandler { call, result ->
when (call.method) { when (call.method) {
"updateRideTracking" -> { "enablePip" -> {
val driverName = call.argument<String>("driverName") ?: "السائق" pipEnabled = true
val driverPhone = call.argument<String>("driverPhone") ?: "" result.success(true)
val carDetails = call.argument<String>("carDetails") ?: ""
val driverLat = call.argument<Double>("driverLat") ?: 0.0
val driverLng = call.argument<Double>("driverLng") ?: 0.0
val passengerLat = call.argument<Double>("passengerLat") ?: 0.0
val passengerLng = call.argument<Double>("passengerLng") ?: 0.0
val destLat = call.argument<Double>("destLat") ?: 0.0
val destLng = call.argument<Double>("destLng") ?: 0.0
val rideState =
call.argument<String>("rideState")
?: "waiting" // "waiting" أو "inProgress"
val estimatedTime = call.argument<Int>("estimatedTime") ?: 5 // بالدقائق
val totalDistance =
call.argument<Double>("totalDistance") ?: 0.0 // بالمتر
RideTrackingService.startOrUpdate(
context = this,
driverName = driverName,
driverPhone = driverPhone,
carDetails = carDetails,
driverLat = driverLat,
driverLng = driverLng,
passengerLat = passengerLat,
passengerLng = passengerLng,
destLat = destLat,
destLng = destLng,
rideState = rideState,
estimatedTime = estimatedTime,
totalDistance = totalDistance
)
result.success(null)
} }
"stopRideTracking" -> { "disablePip" -> {
RideTrackingService.stop(this) pipEnabled = false
result.success(null) result.success(true)
}
"enterPip" -> {
val success = enterPipMode()
result.success(success)
}
"isPipSupported" -> {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
} }
else -> { else -> {
result.notImplemented() result.notImplemented()
@@ -95,6 +74,39 @@ class MainActivity : FlutterFragmentActivity() {
} }
} }
// -------- PiP Helper Methods --------
private fun enterPipMode(): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(9, 16)) // نسبة عمودية مناسبة لعرض الخريطة
.build()
return enterPictureInPictureMode(params)
}
return false
}
// عند ضغط المستخدم على زر الرجوع للشاشة الرئيسية أثناء رحلة نشطة
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (pipEnabled) {
enterPipMode()
}
}
// إعلام Flutter بتغيير وضع PiP
override fun onPictureInPictureModeChanged(
isInPipMode: Boolean,
newConfig: Configuration
) {
super.onPictureInPictureModeChanged(isInPipMode, newConfig)
// يمكن لاحقاً إرسال حدث لـ Flutter لإخفاء/إظهار عناصر الواجهة
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
MethodChannel(messenger, PIP_CHANNEL)
.invokeMethod("onPipChanged", isInPipMode)
}
}
// ---------------- أمن الجهاز (كما عندك تقريباً) ---------------- // ---------------- أمن الجهاز (كما عندك تقريباً) ----------------
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {

View File

@@ -1,373 +0,0 @@
package com.Intaleq.intaleq
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.Looper
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.core.app.NotificationCompat
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationCallback
import com.google.android.gms.location.LocationRequest
import com.google.android.gms.location.LocationResult
import com.google.android.gms.location.LocationServices
import com.google.android.gms.location.Priority
import kotlin.math.*
class RideTrackingService : Service() {
companion object {
private const val TAG = "RideTrackingService"
const val CHANNEL_ID = "TRIP_LIVE_ACTIVITY_CHANNEL"
const val NOTIFICATION_ID = 1001
fun startOrUpdate(
context: Context,
driverName: String,
driverPhone: String,
carDetails: String,
driverLat: Double,
driverLng: Double,
passengerLat: Double,
passengerLng: Double,
destLat: Double,
destLng: Double,
rideState: String,
estimatedTime: Int,
totalDistance: Double
) {
val intent =
Intent(context, RideTrackingService::class.java).apply {
putExtra("driverName", driverName)
putExtra("driverPhone", driverPhone)
putExtra("carDetails", carDetails)
putExtra("driverLat", driverLat)
putExtra("driverLng", driverLng)
putExtra("passengerLat", passengerLat)
putExtra("passengerLng", passengerLng)
putExtra("destLat", destLat)
putExtra("destLng", destLng)
putExtra("rideState", rideState)
putExtra("estimatedTime", estimatedTime)
putExtra("totalDistance", totalDistance)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
} else {
context.startService(intent)
}
}
fun stop(context: Context) {
val intent = Intent(context, RideTrackingService::class.java)
context.stopService(intent)
}
}
private lateinit var fusedLocationClient: FusedLocationProviderClient
private lateinit var locationCallback: LocationCallback
private lateinit var notificationManager: NotificationManager
private var driverLatitude = 0.0
private var driverLongitude = 0.0
private var passengerLatitude = 0.0
private var passengerLongitude = 0.0
private var destinationLatitude = 0.0
private var destinationLongitude = 0.0
private var rideState: String = "waiting"
private var driverName: String = "السائق"
private var driverPhone: String = ""
private var carDetails: String = ""
private var estimatedTimeMinutes: Int = 0
private var totalDistanceMeters: Double = 0.0
private var distanceCoveredMeters: Double = 0.0
private var initialDriverDistanceToPassenger: Double = -1.0
override fun onCreate() {
super.onCreate()
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
setupLocationCallback()
createNotificationChannel()
Log.d(TAG, "Service Created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(TAG, "Service Started")
intent?.let {
driverName = it.getStringExtra("driverName") ?: "السائق"
driverPhone = it.getStringExtra("driverPhone") ?: ""
carDetails = it.getStringExtra("carDetails") ?: ""
driverLatitude = it.getDoubleExtra("driverLat", 0.0)
driverLongitude = it.getDoubleExtra("driverLng", 0.0)
passengerLatitude = it.getDoubleExtra("passengerLat", 0.0)
passengerLongitude = it.getDoubleExtra("passengerLng", 0.0)
destinationLatitude = it.getDoubleExtra("destLat", 0.0)
destinationLongitude = it.getDoubleExtra("destLng", 0.0)
rideState = it.getStringExtra("rideState") ?: "waiting"
estimatedTimeMinutes = it.getIntExtra("estimatedTime", 5)
totalDistanceMeters = it.getDoubleExtra("totalDistance", 0.0)
}
if (rideState == "waiting" && initialDriverDistanceToPassenger < 0) {
val currentDist =
calculateDistance(
passengerLatitude,
passengerLongitude,
driverLatitude,
driverLongitude
)
initialDriverDistanceToPassenger =
if (totalDistanceMeters > currentDist) totalDistanceMeters else currentDist
}
startForeground(NOTIFICATION_ID, createNotification())
startLocationUpdates()
return START_STICKY
}
private fun setupLocationCallback() {
locationCallback =
object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
for (location in locationResult.locations) {
passengerLatitude = location.latitude
passengerLongitude = location.longitude
if (rideState == "inProgress" && totalDistanceMeters > 0) {
val remainingToDest =
calculateDistance(
passengerLatitude,
passengerLongitude,
destinationLatitude,
destinationLongitude
)
distanceCoveredMeters =
(totalDistanceMeters - remainingToDest).coerceAtLeast(0.0)
}
updateNotification()
}
}
}
}
private fun startLocationUpdates() {
val locationRequest =
LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L)
.setMinUpdateIntervalMillis(2000L)
.build()
try {
fusedLocationClient.requestLocationUpdates(
locationRequest,
locationCallback,
Looper.getMainLooper()
)
} catch (e: SecurityException) {
Log.e(TAG, "Location permission denied: ${e.message}")
}
}
private fun createNotification(): Notification {
val contentText = buildContentText()
// 1. جلب التصميم المخصص
val layoutId = resources.getIdentifier("notification_ride_live", "layout", packageName)
// إذا لم يجد الملف، سيطبع خطأ أحمر في اللوج
if (layoutId == 0) {
Log.e(TAG, "❌ خطأ فادح: ملف notification_ride_live.xml غير موجود!")
}
val remoteViews = RemoteViews(packageName, layoutId)
// 2. تعبئة النصوص
val subtitleId = resources.getIdentifier("tv_subtitle", "id", packageName)
val etaId = resources.getIdentifier("tv_eta", "id", packageName)
val titleId = resources.getIdentifier("tv_title", "id", packageName)
if (subtitleId != 0) remoteViews.setTextViewText(subtitleId, "$driverName$carDetails")
if (etaId != 0) remoteViews.setTextViewText(etaId, contentText)
if (titleId != 0)
remoteViews.setTextViewText(
titleId,
if (rideState == "waiting") "السائق في الطريق إليك"
else "رحلة Intaleq جارية"
)
// 3. حساب التقدم (موقع السيارة على الشارع)
var progressIndex = 0
if (rideState == "inProgress" && totalDistanceMeters > 0) {
val percent = (distanceCoveredMeters / totalDistanceMeters).coerceIn(0.0, 1.0)
progressIndex = (percent * 9).toInt()
} else if (rideState == "waiting") {
val remainingToPassenger =
calculateDistance(
passengerLatitude,
passengerLongitude,
driverLatitude,
driverLongitude
)
val total =
if (initialDriverDistanceToPassenger > 0) initialDriverDistanceToPassenger
else remainingToPassenger.coerceAtLeast(1.0)
val percent = 1.0 - (remainingToPassenger / total).coerceIn(0.0, 1.0)
progressIndex = (percent * 9).toInt()
}
// 4. إظهار السيارة في الموضع الصحيح وإخفائها من الباقي
for (i in 0..9) {
val resId = resources.getIdentifier("car_slot_$i", "id", packageName)
if (resId != 0) {
remoteViews.setViewVisibility(
resId,
if (i == progressIndex) View.VISIBLE else View.INVISIBLE
)
}
}
val intent = Intent(this, MainActivity::class.java)
val pendingIntent =
PendingIntent.getActivity(
this,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 5. بناء الإشعار (بدون استخدام setProgress نهائياً لكي لا يظهر الخط الأزرق)
val builder =
NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_map) // أيقونة التطبيق الصغيرة
.setStyle(
NotificationCompat.DecoratedCustomViewStyle()
) // إجباري للتصميم المخصص
.setCustomContentView(remoteViews) // التصميم عند طي الإشعار
.setCustomBigContentView(remoteViews) // التصميم عند سحب الإشعار للأسفل
.setContentIntent(pendingIntent)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
return builder.build()
}
private fun buildContentText(): String {
return when (rideState) {
"waiting" -> {
val distanceToPassenger =
calculateDistance(
passengerLatitude,
passengerLongitude,
driverLatitude,
driverLongitude
)
val etaMinutes =
if (distanceToPassenger > 0) {
(distanceToPassenger / 250.0).toInt().coerceAtLeast(1)
} else {
estimatedTimeMinutes
}
"وصول خلال $etaMinutes د • ${String.format("%.1f", distanceToPassenger / 1000)} كم"
}
"inProgress" -> {
if (totalDistanceMeters > 0) {
val remaining = (totalDistanceMeters - distanceCoveredMeters).coerceAtLeast(0.0)
val progressPercent =
((distanceCoveredMeters / totalDistanceMeters) * 100)
.toInt()
.coerceIn(0, 100)
val etaMinutes =
if (estimatedTimeMinutes > 0) {
((remaining / totalDistanceMeters) * estimatedTimeMinutes)
.toInt()
.coerceAtLeast(1)
} else {
5
}
"المتبقي: ${String.format("%.1f", remaining / 1000)} كم • ~$etaMinutes د"
} else {
"الرحلة قيد التنفيذ..."
}
}
else -> "جاري تحديث موقع الرحلة..."
}
}
private fun updateNotification() {
val notification = createNotification()
notificationManager.notify(NOTIFICATION_ID, notification)
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
CHANNEL_ID,
"تتبع الرحلة",
NotificationManager.IMPORTANCE_HIGH
)
.apply {
description = "إخطارات حية لتقدم الرحلة"
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
setSound(null, null)
}
notificationManager.createNotificationChannel(channel)
}
}
private fun calculateDistance(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double {
val r = 6371000.0
val dLat = Math.toRadians(lat2 - lat1)
val dLng = Math.toRadians(lng2 - lng1)
val a =
sin(dLat / 2) * sin(dLat / 2) +
cos(Math.toRadians(lat1)) *
cos(Math.toRadians(lat2)) *
sin(dLng / 2) *
sin(dLng / 2)
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
return r * c
}
override fun onDestroy() {
super.onDestroy()
try {
fusedLocationClient.removeLocationUpdates(locationCallback)
} catch (e: Exception) {
Log.e(TAG, "Error removing location updates: ${e.message}")
}
Log.d(TAG, "Service Destroyed")
}
override fun onBind(intent: Intent?): IBinder? = null
}

View File

@@ -1,157 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="8dp"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- العنوان الرئيسي -->
<TextView
android:id="@+id/tv_title"
android:text="رحلة Intaleq"
android:textStyle="bold"
android:textSize="14sp"
android:textColor="#FFFFFF"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- سطر بيانات السائق / السيارة -->
<TextView
android:id="@+id/tv_subtitle"
android:text="أحمد محمد • أبيض • ABC 123"
android:textSize="12sp"
android:textColor="#DDDDDD"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="2dp" />
<!-- خط الطريق + سلوطات السيارة -->
<FrameLayout
android:layout_marginTop="6dp"
android:layout_marginBottom="4dp"
android:layout_width="match_parent"
android:layout_height="32dp">
<!-- خلفية الطريق -->
<ImageView
android:id="@+id/img_road"
android:src="@drawable/road_bg"
android:scaleType="fitXY"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<!-- شريط سلوطات السيارة -->
<LinearLayout
android:id="@+id/ll_car_slots"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="8dp"
android:paddingRight="8dp">
<!-- 10 سلوطات للسيارة -->
<!-- كل Slot عبارة عن ImageView، نخلي واحد بس منهم ظاهر حسب الـ progress -->
<ImageView
android:id="@+id/car_slot_0"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_1"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_2"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_3"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_4"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_5"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_6"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_7"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_8"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="invisible"
android:layout_gravity="center_vertical" />
<ImageView
android:id="@+id/car_slot_9"
android:src="@drawable/car_icon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="visible"
android:layout_gravity="center_vertical" />
</LinearLayout>
</FrameLayout>
<!-- سطر الوقت/المسافة المتبقية -->
<TextView
android:id="@+id/tv_eta"
android:text="المتبقي: 8 كم • 12 دقيقة"
android:textSize="12sp"
android:textColor="#CCCCCC"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -1,21 +1,4 @@
// allprojects {
// repositories {
// google()
// mavenCentral()
// }
// }
// rootProject.buildDir = "../build"
// subprojects {
// project.buildDir = "${rootProject.buildDir}/${project.name}"
// }
// subprojects {
// project.evaluationDependsOn(":app")
// }
// tasks.register("clean", Delete) {
// delete rootProject.buildDir
// }
buildscript { buildscript {
ext.kotlin_version = '2.1.0' ext.kotlin_version = '2.1.0'
repositories { repositories {

View File

@@ -3,7 +3,7 @@
archiveVersion = 1; archiveVersion = 1;
classes = { classes = {
}; };
objectVersion = 54; objectVersion = 70;
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
@@ -121,7 +121,7 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
C663DBEB2F50907200D79908 /* Exceptions for "RideWidget" folder in "RideWidgetExtension" target */ = { C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet; isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = ( membershipExceptions = (
Info.plist, Info.plist,
@@ -131,18 +131,7 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
C663DBD82F50907000D79908 /* RideWidget */ = { C663DBD82F50907000D79908 /* RideWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = RideWidget; sourceTree = "<group>"; };
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
C663DBEB2F50907200D79908 /* Exceptions for "RideWidget" folder in "RideWidgetExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = RideWidget;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */ /* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@@ -480,10 +469,14 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";

View File

@@ -1,106 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true /> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Intaleq</string> <string>Intaleq</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>Intaleq</string> <string>Intaleq</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>32</string> <string>33</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
<key>CFBundleTypeRole</key> <key>CFBundleTypeRole</key>
<string>Editor</string> <string>Editor</string>
<key>CFBundleURLName</key> <key>CFBundleURLName</key>
<string>intaleqapp.com</string> <string>intaleqapp.com</string>
<key>CFBundleURLSchemes</key> <key>CFBundleURLSchemes</key>
<array> <array>
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string> <string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
<string>intaleq</string> <string>intaleq</string>
</array> </array>
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.1.32</string> <string>1.1.33</string>
<key>FirebaseAppDelegateProxyEnabled</key> <key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string> <string>NO</string>
<key>FlutterDeepLinkingEnabled</key> <key>FlutterDeepLinkingEnabled</key>
<true /> <true/>
<key>GMSApiKey</key> <key>GMSApiKey</key>
<string>YOUR_API_KEY</string> <string>YOUR_API_KEY</string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>googlechromes</string> <string>googlechromes</string>
<string>comgooglemaps</string> <string>comgooglemaps</string>
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true /> <true/>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app requires access to your camera in order to scan QR codes and capture images <string>This app requires access to your camera in order to scan QR codes and capture images
for uploading and access to connect to a call.</string> for uploading and access to connect to a call.</string>
<key>NSContactsUsageDescription</key> <key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string> <string>This app requires contacts access to function properly.</string>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely authenticate payment accounts.</string> <string>Use Face ID to securely authenticate payment accounts.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>This app needs access to your location to provide you with the best ride experience.
Your location data will be used to find the nearest available cars and connect you with Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string> the closest captain for efficient and convenient rides.</string>
<key>NSLocationAlwaysUsageDescription</key> <key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location.</string> <string>This app needs access to location.</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience. <string>This app needs access to your location to provide you with the best ride experience.
Your location data will be used to find the nearest available cars and connect you with Your location data will be used to find the nearest available cars and connect you with
the closest captain for efficient and convenient rides.</string> the closest captain for efficient and convenient rides.</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add <string>This app requires access to your microphone to record audio, allowing you to add
voice recordings to your photos and videos and access to connect to a call.</string> voice recordings to your photos and videos and access to connect to a call.</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to upload pictures.</string> <string>This app requires access to the photo library to upload pictures.</string>
<key>NSSupportsLiveActivities</key> <key>NSSupportsLiveActivities</key>
<true /> <true/>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true /> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>location</string> <string>location</string>
<string>remote-notification</string> <string>remote-notification</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false /> <false/>
</dict> </dict>
</plist> </plist>

View File

@@ -42,7 +42,7 @@ class AppLink {
static String test = "$server/test.php"; static String test = "$server/test.php";
//===============firebase========================== //===============firebase==========================
static String getTokens = "$server/ride/firebase/get.php"; static String getTokens = "$server/ride/firebase/getTokensPassenger.php.php";
static String getTokenParent = "$server/ride/firebase/getTokenParent.php"; static String getTokenParent = "$server/ride/firebase/getTokenParent.php";
static String addTokens = "$server/ride/firebase/add.php"; static String addTokens = "$server/ride/firebase/add.php";
static String addFingerPrint = "$paymentServer/ride/firebase/add.php"; static String addFingerPrint = "$paymentServer/ride/firebase/add.php";

View File

@@ -4,6 +4,7 @@ import 'dart:math';
import 'package:Intaleq/constant/api_key.dart'; import 'package:Intaleq/constant/api_key.dart';
import 'package:Intaleq/controller/firebase/firbase_messge.dart'; import 'package:Intaleq/controller/firebase/firbase_messge.dart';
import 'package:Intaleq/views/auth/otp_page.dart'; import 'package:Intaleq/views/auth/otp_page.dart';
import 'package:Intaleq/views/widgets/error_snakbar.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:Intaleq/constant/info.dart'; import 'package:Intaleq/constant/info.dart';
@@ -276,37 +277,47 @@ class LoginController extends GetxController {
// مهم: تأكد من passengerID في الـ box // مهم: تأكد من passengerID في الـ box
box.write(BoxName.passengerID, passengerID); box.write(BoxName.passengerID, passengerID);
// 4) نفّذ عمليات مكلفة بالتوازي: getTokens + fingerprint // 4) تنفيذ العمليات بالتوازي: getTokens + fingerprint محلي
final results = await Future.wait([ final results = await Future.wait([
CRUD().get(link: AppLink.getTokens, payload: { CRUD().get(
'passengerID': passengerID, // FIX: لا تستخدم box هنا link: AppLink.getTokens, payload: {'passengerID': passengerID}),
}),
DeviceHelper.getDeviceFingerprint(), DeviceHelper.getDeviceFingerprint(),
]); ]);
await box.write(BoxName.firstTimeLoadKey, 'false');
final tokenResp = results[0];
final fingerPrint = (results[1] ?? '').toString();
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
final tokenResp = results[0];
final localFP = (results[1] ?? '').toString();
await storage.write(key: BoxName.fingerPrint, value: localFP);
await box.write(BoxName.firstTimeLoadKey, 'false');
// ── 5. المقارنة: FCM token + fingerprint ──────────────────────
if (email != '962798583052@intaleqapp.com' && tokenResp != 'failure') { if (email != '962798583052@intaleqapp.com' && tokenResp != 'failure') {
final tokenJson = jsonDecode(tokenResp); final tokenJson = jsonDecode(tokenResp);
final serverToken = tokenJson['message']?['token']?.toString() ?? ''; final serverData = tokenJson['message'] as Map?; // null = أول تسجيل
// Log.print('serverToken: ${serverToken}');
final localFcm = (box.read(BoxName.tokenFCM) ?? '').toString();
// Log.print('localFcm: ${localFcm}');
// 5) اختلاف الجهاز -> تحقّق OTP if (serverData != null) {
if (serverToken.isNotEmpty && serverToken != localFcm) { final serverFCM = serverData['token']?.toString() ?? '';
final goVerify = await _confirmDeviceChangeDialog(); final serverFP = serverData['fingerPrint']?.toString() ?? '';
if (goVerify == true) {
final localFCM = (box.read(BoxName.tokenFCM) ?? '').toString();
// ── اختلاف أي منهما = جهاز مختلف أو تثبيت جديد ─────────
final fcmChanged = serverFCM.isNotEmpty && serverFCM != localFCM;
final fpChanged = serverFP.isNotEmpty && serverFP != localFP;
if (fcmChanged || fpChanged) {
// final goVerify = await _confirmDeviceChangeDialog();
// if (goVerify == true) {
mySnackbarInfo('Device Change Detected'.tr);
//
await Get.to(() => OtpVerificationPage( await Get.to(() => OtpVerificationPage(
phone: data['phone'].toString(), phone: data['phone'].toString(),
deviceToken: fingerPrint, deviceToken: localFP,
token: tokenResp.toString(), token: tokenResp,
ptoken: serverToken, ptoken: serverFCM, // نمرر FCM القديم للـ OTP controller
)); ));
// بعد العودة من OTP (نجح/فشل)، أخرج من الميثود كي لا يحصل offAll مرتين return; // لا تكمل — الـ OTP controller يتولى الانتقال
return; // }
} }
} }
} }
@@ -359,18 +370,18 @@ class LoginController extends GetxController {
} }
} }
Future<bool?> _confirmDeviceChangeDialog() { // Future<bool?> _confirmDeviceChangeDialog() {
return Get.defaultDialog<bool>( // return Get.defaultDialog<bool>(
barrierDismissible: false, // barrierDismissible: false,
title: 'Device Change Detected'.tr, // title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr, // middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr, // textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white, // confirmTextColor: Colors.white,
onConfirm: () => Get.back(result: true), // onConfirm: () => Get.back(result: true),
textCancel: 'Cancel'.tr, // textCancel: 'Cancel'.tr,
onCancel: () => Get.back(result: false), // onCancel: () => Get.back(result: false),
); // );
} // }
void login() async { void login() async {
isloading = true; isloading = true;

View File

@@ -45,7 +45,7 @@ import '../../main.dart';
import '../../models/model/locations.dart'; import '../../models/model/locations.dart';
import '../../models/model/painter_copoun.dart'; import '../../models/model/painter_copoun.dart';
import '../../print.dart'; import '../../print.dart';
import '../../services/ride_tracking_native.dart'; import '../../services/pip_service.dart';
import '../../views/home/map_widget.dart/cancel_raide_page.dart'; import '../../views/home/map_widget.dart/cancel_raide_page.dart';
import '../../views/home/map_widget.dart/car_details_widget_to_go.dart'; import '../../views/home/map_widget.dart/car_details_widget_to_go.dart';
import '../../views/home/map_widget.dart/select_driver_mishwari.dart'; import '../../views/home/map_widget.dart/select_driver_mishwari.dart';
@@ -357,49 +357,83 @@ class MapPassengerController extends GetxController {
.setTransports(['websocket']) .setTransports(['websocket'])
.disableAutoConnect() .disableAutoConnect()
.setQuery({'id': passengerId}) .setQuery({'id': passengerId})
// ✅ [FIX] إعادة اتصال شبه-لانهائية (999 محاولة) بدلاً من 20
.setReconnectionAttempts(20) .setReconnectionAttempts(20)
.setReconnectionDelay(2400) // ✅ [FIX] تأخير أقل (1.5 ثانية) مع حد أقصى (8 ثواني) للتسريع
// ✅ أضف هذا السطر لحل مشكلة الـ Heartbeat مع PHPSocketIO .setReconnectionDelay(1500)
.setReconnectionDelayMax(8000)
.enableReconnection()
.setExtraHeaders({'Connection': 'Upgrade'}) .setExtraHeaders({'Connection': 'Upgrade'})
.build(), .build(),
); );
socket.connect(); socket.connect();
// ✅ إضافة النبضة (Heartbeat) لمنع السيرفر من قطع الاتصال
_heartbeatTimer?.cancel(); // إيقاف أي نبضة قديمة
_heartbeatTimer = Timer.periodic(const Duration(seconds: 25), (timer) {
if (isSocketConnected && socket != null && socket!.connected) {
socket!.emit('heartbeat', {'passenger_id': passengerId});
// Log.print("💓 Socket Heartbeat sent"); // اختياري للتأكد أنه يعمل
} else {
timer.cancel(); // إيقاف النبضة إذا انقطع السوكيت
}
});
// ✅ معالج الاتصال
// ✅ معالج الاتصال الأول
socket.onConnect((_) { socket.onConnect((_) {
Log.print("✅ Socket Connected Successfully"); Log.print("✅ Socket Connected Successfully");
isSocketConnected = true; isSocketConnected = true;
_reconnectAttempts = 0; _reconnectAttempts = 0;
_startHeartbeat(); // ← أضف هذا _startHeartbeat();
// ✅ [FIX] الاشتراك مجدداً في أحداث الرحلة عند كل اتصال
if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) {
socket.emit('subscribe_driver_location', {
'ride_id': rideId,
'driver_id': driverId,
});
Log.print("📡 Re-subscribed to driver location after connect");
}
update(); update();
}); });
// دالة منفصلة للـ heartbeat
// ⚠️ معالج الانقطاع // ⚠️ معالج الانقطاع
socket.onDisconnect((_) { socket.onDisconnect((_) {
Log.print("⚠️ Socket Disconnected"); Log.print("⚠️ Socket Disconnected — Auto-Reconnect will handle it");
isSocketConnected = false; isSocketConnected = false;
// تفعيل Polling أسرع كـ Fallback // تفعيل Polling أسرع كـ Fallback مؤقت (سيتم إيقافه عند عودة الاتصال)
if (_isActiveRideState()) { if (_isActiveRideState()) {
Log.print("🔄 Switching to Fast Polling Mode (6s interval)"); Log.print("🔄 Enabling Fast Polling Fallback (4s) until reconnect...");
_startMasterTimerWithInterval(4); _startMasterTimerWithInterval(4);
} }
update(); update();
}); });
// 🔁 [FIX] معالج إعادة الاتصال الناجحة
socket.onReconnect((_) {
Log.print("🔁 Socket Reconnected Successfully!");
isSocketConnected = true;
_reconnectAttempts = 0;
// استئناف النبضة فوراً
_startHeartbeat();
// إعادة الاشتراك في أحداث الرحلة
if (rideId != null && rideId != 'yet' && driverId.isNotEmpty) {
socket.emit('subscribe_driver_location', {
'ride_id': rideId,
'driver_id': driverId,
});
Log.print("📡 Re-subscribed to driver location after reconnect");
}
// ✅ [FIX] إيقاف الـ Fast Polling لأن السوكيت عاد
if (_isActiveRideState()) {
Log.print("✅ Socket back online — stopping Fast Polling Fallback");
_masterTimer?.cancel();
_masterTimer = null;
}
update();
});
// 🔄 [FIX] معالج محاولات إعادة الاتصال (للتشخيص)
socket.onReconnectAttempt((attemptNumber) {
Log.print("🔄 Socket Reconnect Attempt #$attemptNumber...");
});
// ❌ معالج الأخطاء // ❌ معالج الأخطاء
socket.onError((error) { socket.onError((error) {
Log.print("❌ Socket Error: $error"); Log.print("❌ Socket Error: $error");
@@ -724,6 +758,7 @@ class MapPassengerController extends GetxController {
if (Get.isDialogOpen == true) Get.back(); if (Get.isDialogOpen == true) Get.back();
await RideLiveNotification.cancel(); await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر
PipService.disablePip(); // ✅ إيقاف PiP عند انتهاء الرحلة
if (Get.isDialogOpen == true) Get.back(); if (Get.isDialogOpen == true) Get.back();
await RideLiveNotification.cancel(); await RideLiveNotification.cancel();
Get.defaultDialog( Get.defaultDialog(
@@ -771,6 +806,7 @@ class MapPassengerController extends GetxController {
stopAllTimers(); stopAllTimers();
await RideLiveNotification.cancel(); await RideLiveNotification.cancel();
IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر IosLiveActivityService.endRideActivity(); // ✅ أضف هذا السطر
PipService.disablePip(); // ✅ إيقاف PiP
_isCancelProcessed = false; _isCancelProcessed = false;
currentRideState.value = RideState.noRide; currentRideState.value = RideState.noRide;
resetAllMapStates(); resetAllMapStates();
@@ -959,6 +995,7 @@ class MapPassengerController extends GetxController {
'tone1', 'tone1',
); );
IosLiveActivityService.endRideActivity(); IosLiveActivityService.endRideActivity();
PipService.disablePip(); // ✅ إيقاف PiP
await RideLiveNotification.cancel(); await RideLiveNotification.cancel();
// 5. استخراج البيانات والانتقال // 5. استخراج البيانات والانتقال
if (driverList.length >= 4) { if (driverList.length >= 4) {
@@ -1272,20 +1309,24 @@ class MapPassengerController extends GetxController {
timeToPassengerFromDriverAfterApplied; // مثلاً من السيرفر timeToPassengerFromDriverAfterApplied; // مثلاً من السيرفر
final double distanceDriverToPassengerMeters = final double distanceDriverToPassengerMeters =
double.parse(distanceByPassenger); double.parse(distanceByPassenger);
await RideTrackingNative.updateRideTracking( // [PiP] تم تعطيل الإشعار المستمر القديم (Foreground Service) واستبداله بـ PiP
driverName: driverName, // await RideTrackingNative.updateRideTracking(
driverPhone: driverPhone, // driverName: driverName,
carDetails: '$make$carColor$licensePlate', // driverPhone: driverPhone,
driverLat: driverCarsLocationToPassengerAfterApplied.last.latitude, // carDetails: '$make • $carColor • $licensePlate',
driverLng: driverCarsLocationToPassengerAfterApplied.last.longitude, // driverLat: driverCarsLocationToPassengerAfterApplied.last.latitude,
passengerLat: passengerLocation.latitude, // driverLng: driverCarsLocationToPassengerAfterApplied.last.longitude,
passengerLng: passengerLocation.longitude, // passengerLat: passengerLocation.latitude,
destLat: myDestination.latitude, // passengerLng: passengerLocation.longitude,
destLng: myDestination.longitude, // destLat: myDestination.latitude,
rideState: 'waiting', // يعني السائق بالطريق للراكب // destLng: myDestination.longitude,
estimatedTimeMinutes: (timeToPassengerSeconds / 60).round(), // rideState: 'waiting',
totalDistanceMeters: distanceDriverToPassengerMeters, // estimatedTimeMinutes: (timeToPassengerSeconds / 60).round(),
); // totalDistanceMeters: distanceDriverToPassengerMeters,
// );
// [PiP] تفعيل PiP عند بدء الرحلة (سيدخل وضع النافذة العائمة عند خروج المستخدم)
PipService.enablePip();
// 6. بدء تتبع الموقع الدوري (Polling Backup + Smart Rerouting) // 6. بدء تتبع الموقع الدوري (Polling Backup + Smart Rerouting)
// سيبدأ العمل بعد 6 ثواني // سيبدأ العمل بعد 6 ثواني
@@ -1918,21 +1959,21 @@ class MapPassengerController extends GetxController {
durationToRide; // موجود عندك من التايمر durationToRide; // موجود عندك من التايمر
final double totalDistanceMeters = double.parse(distanceByPassenger); final double totalDistanceMeters = double.parse(distanceByPassenger);
// 2) استدعاء خدمة الأندرويد لتحديث الإشعار لحالة "inProgress" // [PiP] تم تعطيل الإشعار المستمر القديم (Foreground Service) واستبداله بـ PiP
await RideTrackingNative.updateRideTracking( // await RideTrackingNative.updateRideTracking(
driverName: driverName, // driverName: driverName,
driverPhone: driverPhone, // driverPhone: driverPhone,
carDetails: carDetails, // carDetails: carDetails,
driverLat: driverLat, // driverLat: driverLat,
driverLng: driverLng, // driverLng: driverLng,
passengerLat: passengerLat, // passengerLat: passengerLat,
passengerLng: passengerLng, // passengerLng: passengerLng,
destLat: destLat, // destLat: destLat,
destLng: destLng, // destLng: destLng,
rideState: 'inProgress', // rideState: 'inProgress',
estimatedTimeMinutes: (timeToDestinationSeconds / 60).round(), // estimatedTimeMinutes: (timeToDestinationSeconds / 60).round(),
totalDistanceMeters: totalDistanceMeters, // totalDistanceMeters: totalDistanceMeters,
); // );
// 3) بدء التايمر الداخلي الخاص بك (للـ ETA داخل التطبيق نفسه) // 3) بدء التايمر الداخلي الخاص بك (للـ ETA داخل التطبيق نفسه)
rideIsBeginPassengerTimer(); rideIsBeginPassengerTimer();
@@ -4894,6 +4935,7 @@ Intaleq Team''';
currentRideState.value = RideState.cancelled; currentRideState.value = RideState.cancelled;
await RideLiveNotification.cancel(); // إغلاق أندرويد await RideLiveNotification.cancel(); // إغلاق أندرويد
IosLiveActivityService.endRideActivity(); // ✅ إغلاق iOS IosLiveActivityService.endRideActivity(); // ✅ إغلاق iOS
PipService.disablePip(); // ✅ إيقاف PiP عند الإلغاء
// 4. الاتصال بالسيرفر لإلغاء الرحلة وإبلاغ السائق // 4. الاتصال بالسيرفر لإلغاء الرحلة وإبلاغ السائق
if (rideId != 'yet' && rideId != null) { if (rideId != 'yet' && rideId != null) {

View File

@@ -4,7 +4,7 @@ class Log {
Log._(); Log._();
static void print(String value, {StackTrace? stackTrace}) { static void print(String value, {StackTrace? stackTrace}) {
developer.log(value, name: 'LOG', stackTrace: stackTrace); // developer.log(value, name: 'LOG', stackTrace: stackTrace);
} }
static Object? inspect(Object? object) { static Object? inspect(Object? object) {

View File

@@ -0,0 +1,61 @@
import 'dart:io';
import 'package:flutter/services.dart';
/// خدمة التحكم بوضع النافذة العائمة (Picture-in-Picture) على أندرويد.
/// تُستدعى عند بدء الرحلة لتفعيل PiP تلقائياً عند خروج المستخدم من التطبيق.
class PipService {
static const MethodChannel _channel = MethodChannel('intaleq/pip');
/// هل وضع PiP مدعوم على هذا الجهاز؟
static Future<bool> isPipSupported() async {
if (!Platform.isAndroid) return false;
try {
final result = await _channel.invokeMethod<bool>('isPipSupported');
return result ?? false;
} catch (e) {
return false;
}
}
/// تفعيل الدخول التلقائي لوضع PiP عند الخروج (أثناء الرحلة)
static Future<void> enablePip() async {
if (!Platform.isAndroid) return;
try {
await _channel.invokeMethod('enablePip');
} catch (e) {
print('PiP enable error: \$e');
}
}
/// تعطيل الدخول التلقائي لوضع PiP (بعد انتهاء الرحلة)
static Future<void> disablePip() async {
if (!Platform.isAndroid) return;
try {
await _channel.invokeMethod('disablePip');
} catch (e) {
print('PiP disable error: \$e');
}
}
/// الدخول يدوياً لوضع PiP
static Future<bool> enterPip() async {
if (!Platform.isAndroid) return false;
try {
final result = await _channel.invokeMethod<bool>('enterPip');
return result ?? false;
} catch (e) {
print('PiP enter error: \$e');
return false;
}
}
/// الاستماع لتغيير وضع PiP (الدخول/الخروج)
static void listenToPipChanges(Function(bool isInPip) onChanged) {
_channel.setMethodCallHandler((call) async {
if (call.method == 'onPipChanged') {
final isInPip = call.arguments as bool;
onChanged(isInPip);
}
});
}
}