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

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

View File

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

View File

@@ -1,8 +1,12 @@
package com.Intaleq.intaleq
import android.app.AlertDialog
import android.app.PictureInPictureParams
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.util.Rational
import android.widget.LinearLayout
import android.widget.ProgressBar
import android.widget.TextView
@@ -21,8 +25,9 @@ class MainActivity : FlutterFragmentActivity() {
private val SECURITY_CHANNEL_NAME = "com.Intaleq.intaleq/security"
private lateinit var securityChannel: MethodChannel
// قناة تتبّع الرحلة (Live Activity على أندرويد)
private val RIDE_TRACKING_CHANNEL = "intaleq/ride_tracking"
// قناة PiP الجديدة
private val PIP_CHANNEL = "intaleq/pip"
private var pipEnabled = false // هل الرحلة نشطة ويجب تفعيل PiP عند الخروج؟
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
@@ -43,50 +48,24 @@ class MainActivity : FlutterFragmentActivity() {
}
}
// -------- 2) قناة تتبع الرحلة (Ride Tracking) --------
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, RIDE_TRACKING_CHANNEL)
// -------- 2) قناة PiP (Picture-in-Picture) --------
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL)
.setMethodCallHandler { call, result ->
when (call.method) {
"updateRideTracking" -> {
val driverName = call.argument<String>("driverName") ?: "السائق"
val driverPhone = call.argument<String>("driverPhone") ?: ""
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)
"enablePip" -> {
pipEnabled = true
result.success(true)
}
"stopRideTracking" -> {
RideTrackingService.stop(this)
result.success(null)
"disablePip" -> {
pipEnabled = false
result.success(true)
}
"enterPip" -> {
val success = enterPipMode()
result.success(success)
}
"isPipSupported" -> {
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
}
else -> {
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?) {

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 {
ext.kotlin_version = '2.1.0'
repositories {

View File

@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objectVersion = 70;
objects = {
/* Begin PBXBuildFile section */
@@ -121,7 +121,7 @@
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
C663DBEB2F50907200D79908 /* Exceptions for "RideWidget" folder in "RideWidgetExtension" target */ = {
C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
@@ -131,18 +131,7 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
C663DBD82F50907000D79908 /* RideWidget */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
C663DBEB2F50907200D79908 /* Exceptions for "RideWidget" folder in "RideWidgetExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = RideWidget;
sourceTree = "<group>";
};
C663DBD82F50907000D79908 /* RideWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = RideWidget; sourceTree = "<group>"; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -480,10 +469,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
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"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Intaleq</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Intaleq</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>32</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>intaleqapp.com</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
<string>intaleq</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.1.32</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string>
<key>FlutterDeepLinkingEnabled</key>
<true />
<key>GMSApiKey</key>
<string>YOUR_API_KEY</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSCameraUsageDescription</key>
<string>This app requires access to your camera in order to scan QR codes and capture images
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Intaleq</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Intaleq</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>33</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>intaleqapp.com</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
<string>intaleq</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.1.33</string>
<key>FirebaseAppDelegateProxyEnabled</key>
<string>NO</string>
<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>GMSApiKey</key>
<string>YOUR_API_KEY</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>googlechromes</string>
<string>comgooglemaps</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<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>
<key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely authenticate payment accounts.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience.
<key>NSContactsUsageDescription</key>
<string>This app requires contacts access to function properly.</string>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to securely authenticate payment accounts.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<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
the closest captain for efficient and convenient rides.</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>This app needs access to your location to provide you with the best ride experience.
<key>NSLocationAlwaysUsageDescription</key>
<string>This app needs access to location.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<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
the closest captain for efficient and convenient rides.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app requires access to your microphone to record audio, allowing you to add
<key>NSMicrophoneUsageDescription</key>
<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>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to upload pictures.</string>
<key>NSSupportsLiveActivities</key>
<true />
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>location</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
</dict>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app requires access to the photo library to upload pictures.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>location</string>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</plist>

View File

@@ -42,7 +42,7 @@ class AppLink {
static String test = "$server/test.php";
//===============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 addTokens = "$server/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/controller/firebase/firbase_messge.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:Intaleq/constant/info.dart';
@@ -276,37 +277,47 @@ class LoginController extends GetxController {
// مهم: تأكد من passengerID في الـ box
box.write(BoxName.passengerID, passengerID);
// 4) نفّذ عمليات مكلفة بالتوازي: getTokens + fingerprint
// 4) تنفيذ العمليات بالتوازي: getTokens + fingerprint محلي
final results = await Future.wait([
CRUD().get(link: AppLink.getTokens, payload: {
'passengerID': passengerID, // FIX: لا تستخدم box هنا
}),
CRUD().get(
link: AppLink.getTokens, payload: {'passengerID': passengerID}),
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') {
final tokenJson = jsonDecode(tokenResp);
final serverToken = tokenJson['message']?['token']?.toString() ?? '';
// Log.print('serverToken: ${serverToken}');
final localFcm = (box.read(BoxName.tokenFCM) ?? '').toString();
// Log.print('localFcm: ${localFcm}');
final serverData = tokenJson['message'] as Map?; // null = أول تسجيل
// 5) اختلاف الجهاز -> تحقّق OTP
if (serverToken.isNotEmpty && serverToken != localFcm) {
final goVerify = await _confirmDeviceChangeDialog();
if (goVerify == true) {
if (serverData != null) {
final serverFCM = serverData['token']?.toString() ?? '';
final serverFP = serverData['fingerPrint']?.toString() ?? '';
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(
phone: data['phone'].toString(),
deviceToken: fingerPrint,
token: tokenResp.toString(),
ptoken: serverToken,
deviceToken: localFP,
token: tokenResp,
ptoken: serverFCM, // نمرر FCM القديم للـ OTP controller
));
// بعد العودة من OTP (نجح/فشل)، أخرج من الميثود كي لا يحصل offAll مرتين
return;
return; // لا تكمل — الـ OTP controller يتولى الانتقال
// }
}
}
}
@@ -359,18 +370,18 @@ class LoginController extends GetxController {
}
}
Future<bool?> _confirmDeviceChangeDialog() {
return Get.defaultDialog<bool>(
barrierDismissible: false,
title: 'Device Change Detected'.tr,
middleText: 'Please verify your identity'.tr,
textConfirm: 'Verify'.tr,
confirmTextColor: Colors.white,
onConfirm: () => Get.back(result: true),
textCancel: 'Cancel'.tr,
onCancel: () => Get.back(result: false),
);
}
// Future<bool?> _confirmDeviceChangeDialog() {
// return Get.defaultDialog<bool>(
// barrierDismissible: false,
// title: 'Device Change Detected'.tr,
// middleText: 'Please verify your identity'.tr,
// textConfirm: 'Verify'.tr,
// confirmTextColor: Colors.white,
// onConfirm: () => Get.back(result: true),
// textCancel: 'Cancel'.tr,
// onCancel: () => Get.back(result: false),
// );
// }
void login() async {
isloading = true;

View File

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

View File

@@ -4,7 +4,7 @@ class Log {
Log._();
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) {

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);
}
});
}
}