From e325405dff39ef53e172c66ba4caab634452617d Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 3 Apr 2026 16:23:14 +0300 Subject: [PATCH] 2026-04-03-maplibra come next --- android/app/build.gradle | 5 +- android/app/src/main/AndroidManifest.xml | 48 +-- .../com/Intaleq/intaleq/MainActivity.kt | 98 +++-- .../Intaleq/intaleq/RideTrackingService.kt | 373 ------------------ .../res/layout/notification_ride_live.xml | 157 -------- android/build.gradle | 17 - ios/Runner.xcodeproj/project.pbxproj | 21 +- ios/Runner/Info.plist | 192 ++++----- lib/constant/links.dart | 2 +- lib/controller/auth/login_controller.dart | 77 ++-- .../home/map_passenger_controller.dart | 140 ++++--- lib/print.dart | 2 +- lib/services/pip_service.dart | 61 +++ 13 files changed, 363 insertions(+), 830 deletions(-) delete mode 100644 android/app/src/main/kotlin/com/Intaleq/intaleq/RideTrackingService.kt delete mode 100644 android/app/src/main/res/layout/notification_ride_live.xml create mode 100644 lib/services/pip_service.dart diff --git a/android/app/build.gradle b/android/app/build.gradle index 2b26425..cd063ec 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -30,6 +30,7 @@ android { version "3.22.1" } } + 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" diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 2f6b853..7014485 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - @@ -14,9 +13,6 @@ - - - @@ -30,7 +26,6 @@ android:usesCleartextTraffic="false" android:networkSecurityConfig="@xml/network_security_config"> - @@ -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"> - - - - - - - + - - - @@ -89,34 +72,16 @@ - - - - - - - - - - - - - - - - - - @@ -129,12 +94,7 @@ - - + when (call.method) { - "updateRideTracking" -> { - val driverName = call.argument("driverName") ?: "السائق" - val driverPhone = call.argument("driverPhone") ?: "" - val carDetails = call.argument("carDetails") ?: "" - - val driverLat = call.argument("driverLat") ?: 0.0 - val driverLng = call.argument("driverLng") ?: 0.0 - val passengerLat = call.argument("passengerLat") ?: 0.0 - val passengerLng = call.argument("passengerLng") ?: 0.0 - val destLat = call.argument("destLat") ?: 0.0 - val destLng = call.argument("destLng") ?: 0.0 - - val rideState = - call.argument("rideState") - ?: "waiting" // "waiting" أو "inProgress" - val estimatedTime = call.argument("estimatedTime") ?: 5 // بالدقائق - val totalDistance = - call.argument("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?) { diff --git a/android/app/src/main/kotlin/com/Intaleq/intaleq/RideTrackingService.kt b/android/app/src/main/kotlin/com/Intaleq/intaleq/RideTrackingService.kt deleted file mode 100644 index 97ecffb..0000000 --- a/android/app/src/main/kotlin/com/Intaleq/intaleq/RideTrackingService.kt +++ /dev/null @@ -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 -} diff --git a/android/app/src/main/res/layout/notification_ride_live.xml b/android/app/src/main/res/layout/notification_ride_live.xml deleted file mode 100644 index 91dba88..0000000 --- a/android/app/src/main/res/layout/notification_ride_live.xml +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index b8001a4..c5fcbd0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -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 { diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e8eaec3..6efba93 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; - }; + C663DBD82F50907000D79908 /* RideWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = RideWidget; sourceTree = ""; }; /* 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"; diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1ed3bac..c4d0edd 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,106 +1,106 @@ - - CADisableMinimumFrameDurationOnPhone - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Intaleq - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - Intaleq - CFBundlePackageType - APPL - CFBundleShortVersionString - 32 - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLName - intaleqapp.com - CFBundleURLSchemes - - com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d - intaleq - - - - CFBundleVersion - 1.1.32 - FirebaseAppDelegateProxyEnabled - NO - FlutterDeepLinkingEnabled - - GMSApiKey - YOUR_API_KEY - LSApplicationQueriesSchemes - - googlechromes - comgooglemaps - - LSRequiresIPhoneOS - - NSCameraUsageDescription - This app requires access to your camera in order to scan QR codes and capture images + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Intaleq + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Intaleq + CFBundlePackageType + APPL + CFBundleShortVersionString + 33 + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + intaleqapp.com + CFBundleURLSchemes + + com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d + intaleq + + + + CFBundleVersion + 1.1.33 + FirebaseAppDelegateProxyEnabled + NO + FlutterDeepLinkingEnabled + + GMSApiKey + YOUR_API_KEY + LSApplicationQueriesSchemes + + googlechromes + comgooglemaps + + LSRequiresIPhoneOS + + NSCameraUsageDescription + 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. - NSContactsUsageDescription - This app requires contacts access to function properly. - NSFaceIDUsageDescription - Use Face ID to securely authenticate payment accounts. - NSLocationAlwaysAndWhenInUseUsageDescription - This app needs access to your location to provide you with the best ride experience. + NSContactsUsageDescription + This app requires contacts access to function properly. + NSFaceIDUsageDescription + Use Face ID to securely authenticate payment accounts. + NSLocationAlwaysAndWhenInUseUsageDescription + 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. - NSLocationAlwaysUsageDescription - This app needs access to location. - NSLocationWhenInUseUsageDescription - This app needs access to your location to provide you with the best ride experience. + NSLocationAlwaysUsageDescription + This app needs access to location. + NSLocationWhenInUseUsageDescription + 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. - NSMicrophoneUsageDescription - This app requires access to your microphone to record audio, allowing you to add + NSMicrophoneUsageDescription + 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. - NSPhotoLibraryUsageDescription - This app requires access to the photo library to upload pictures. - NSSupportsLiveActivities - - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - location - remote-notification - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - + NSPhotoLibraryUsageDescription + This app requires access to the photo library to upload pictures. + NSSupportsLiveActivities + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + location + remote-notification + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/lib/constant/links.dart b/lib/constant/links.dart index 96aaf6d..3ecb583 100644 --- a/lib/constant/links.dart +++ b/lib/constant/links.dart @@ -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"; diff --git a/lib/controller/auth/login_controller.dart b/lib/controller/auth/login_controller.dart index 25eb9c9..b78794a 100644 --- a/lib/controller/auth/login_controller.dart +++ b/lib/controller/auth/login_controller.dart @@ -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 _confirmDeviceChangeDialog() { - return Get.defaultDialog( - 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 _confirmDeviceChangeDialog() { + // return Get.defaultDialog( + // 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; diff --git a/lib/controller/home/map_passenger_controller.dart b/lib/controller/home/map_passenger_controller.dart index 66ce2fd..98d37e6 100644 --- a/lib/controller/home/map_passenger_controller.dart +++ b/lib/controller/home/map_passenger_controller.dart @@ -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) { diff --git a/lib/print.dart b/lib/print.dart index a3d59f6..63efb2d 100644 --- a/lib/print.dart +++ b/lib/print.dart @@ -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) { diff --git a/lib/services/pip_service.dart b/lib/services/pip_service.dart new file mode 100644 index 0000000..79f3e55 --- /dev/null +++ b/lib/services/pip_service.dart @@ -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 isPipSupported() async { + if (!Platform.isAndroid) return false; + try { + final result = await _channel.invokeMethod('isPipSupported'); + return result ?? false; + } catch (e) { + return false; + } + } + + /// تفعيل الدخول التلقائي لوضع PiP عند الخروج (أثناء الرحلة) + static Future enablePip() async { + if (!Platform.isAndroid) return; + try { + await _channel.invokeMethod('enablePip'); + } catch (e) { + print('PiP enable error: \$e'); + } + } + + /// تعطيل الدخول التلقائي لوضع PiP (بعد انتهاء الرحلة) + static Future disablePip() async { + if (!Platform.isAndroid) return; + try { + await _channel.invokeMethod('disablePip'); + } catch (e) { + print('PiP disable error: \$e'); + } + } + + /// الدخول يدوياً لوضع PiP + static Future enterPip() async { + if (!Platform.isAndroid) return false; + try { + final result = await _channel.invokeMethod('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); + } + }); + } +}