2026-04-03-maplibra come next
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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?) {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
61
lib/services/pip_service.dart
Normal file
61
lib/services/pip_service.dart
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user