2026-04-03-maplibra come next
This commit is contained in:
@@ -31,6 +31,7 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8
|
||||
}
|
||||
@@ -47,8 +48,8 @@ android {
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdkVersion = 24
|
||||
targetSdk = 36
|
||||
versionCode = 60
|
||||
versionName = '1.1.60'
|
||||
versionCode = 63
|
||||
versionName = '1.1.63'
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.Intaleq.intaleq">
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
@@ -14,9 +13,6 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||
|
||||
<uses-feature android:name="android.hardware.camera" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" />
|
||||
@@ -30,7 +26,6 @@
|
||||
android:usesCleartextTraffic="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!-- ✅ مهم جداً: تعريف أن المشروع يستخدم V2 Embedding -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
@@ -39,49 +34,37 @@
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
|
||||
<!-- Flutter -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme" />
|
||||
|
||||
<!-- Launcher -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- 🔗 App Links -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="intaleqapp.com"
|
||||
android:pathPrefix="/" />
|
||||
|
||||
<data
|
||||
android:scheme="https"
|
||||
android:host="www.intaleqapp.com"
|
||||
<data android:scheme="https" android:host="intaleqapp.com" android:pathPrefix="/" />
|
||||
<data android:scheme="https" android:host="www.intaleqapp.com"
|
||||
android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- 🔗 Custom Scheme -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="intaleq" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- 🔗 Intercept Geo URIs (geo:lat,lng) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -89,34 +72,16 @@
|
||||
<data android:scheme="geo" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- 🔗 Intercept External Map URLs (Google/Apple) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="http" android:host="maps.google.com" />
|
||||
<data android:scheme="https" android:host="maps.google.com" />
|
||||
<data android:scheme="https" android:host="maps.apple.com" />
|
||||
<data android:scheme="https" android:host="goo.gl" />
|
||||
<data android:scheme="http" android:host="goo.gl" />
|
||||
<data android:scheme="https" android:host="maps.app.goo.gl" />
|
||||
<data android:scheme="http" android:host="maps.app.goo.gl" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<!-- Google Maps API -->
|
||||
<meta-data
|
||||
android:name="com.google.android.geo.API_KEY"
|
||||
android:value="${mapsApiKey}" />
|
||||
|
||||
<!-- Firebase Notification Channel -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||
android:value="@string/default_notification_channel_id" />
|
||||
|
||||
<!-- Local Notifications Receivers -->
|
||||
<receiver
|
||||
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver"
|
||||
android:exported="false" />
|
||||
@@ -129,12 +94,7 @@
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
<service
|
||||
android:name=".RideTrackingService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
<!-- UCrop -->
|
||||
|
||||
<activity
|
||||
android:name="com.yalantis.ucrop.UCropActivity"
|
||||
android:screenOrientation="portrait"
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package com.Intaleq.intaleq
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
@@ -21,8 +25,9 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
private val SECURITY_CHANNEL_NAME = "com.Intaleq.intaleq/security"
|
||||
private lateinit var securityChannel: MethodChannel
|
||||
|
||||
// قناة تتبّع الرحلة (Live Activity على أندرويد)
|
||||
private val RIDE_TRACKING_CHANNEL = "intaleq/ride_tracking"
|
||||
// قناة PiP الجديدة
|
||||
private val PIP_CHANNEL = "intaleq/pip"
|
||||
private var pipEnabled = false // هل الرحلة نشطة ويجب تفعيل PiP عند الخروج؟
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
@@ -43,50 +48,24 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// -------- 2) قناة تتبع الرحلة (Ride Tracking) --------
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, RIDE_TRACKING_CHANNEL)
|
||||
// -------- 2) قناة PiP (Picture-in-Picture) --------
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, PIP_CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"updateRideTracking" -> {
|
||||
val driverName = call.argument<String>("driverName") ?: "السائق"
|
||||
val driverPhone = call.argument<String>("driverPhone") ?: ""
|
||||
val carDetails = call.argument<String>("carDetails") ?: ""
|
||||
|
||||
val driverLat = call.argument<Double>("driverLat") ?: 0.0
|
||||
val driverLng = call.argument<Double>("driverLng") ?: 0.0
|
||||
val passengerLat = call.argument<Double>("passengerLat") ?: 0.0
|
||||
val passengerLng = call.argument<Double>("passengerLng") ?: 0.0
|
||||
val destLat = call.argument<Double>("destLat") ?: 0.0
|
||||
val destLng = call.argument<Double>("destLng") ?: 0.0
|
||||
|
||||
val rideState =
|
||||
call.argument<String>("rideState")
|
||||
?: "waiting" // "waiting" أو "inProgress"
|
||||
val estimatedTime = call.argument<Int>("estimatedTime") ?: 5 // بالدقائق
|
||||
val totalDistance =
|
||||
call.argument<Double>("totalDistance") ?: 0.0 // بالمتر
|
||||
|
||||
RideTrackingService.startOrUpdate(
|
||||
context = this,
|
||||
driverName = driverName,
|
||||
driverPhone = driverPhone,
|
||||
carDetails = carDetails,
|
||||
driverLat = driverLat,
|
||||
driverLng = driverLng,
|
||||
passengerLat = passengerLat,
|
||||
passengerLng = passengerLng,
|
||||
destLat = destLat,
|
||||
destLng = destLng,
|
||||
rideState = rideState,
|
||||
estimatedTime = estimatedTime,
|
||||
totalDistance = totalDistance
|
||||
)
|
||||
|
||||
result.success(null)
|
||||
"enablePip" -> {
|
||||
pipEnabled = true
|
||||
result.success(true)
|
||||
}
|
||||
"stopRideTracking" -> {
|
||||
RideTrackingService.stop(this)
|
||||
result.success(null)
|
||||
"disablePip" -> {
|
||||
pipEnabled = false
|
||||
result.success(true)
|
||||
}
|
||||
"enterPip" -> {
|
||||
val success = enterPipMode()
|
||||
result.success(success)
|
||||
}
|
||||
"isPipSupported" -> {
|
||||
result.success(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
@@ -95,6 +74,39 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// -------- PiP Helper Methods --------
|
||||
|
||||
private fun enterPipMode(): Boolean {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(Rational(9, 16)) // نسبة عمودية مناسبة لعرض الخريطة
|
||||
.build()
|
||||
return enterPictureInPictureMode(params)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// عند ضغط المستخدم على زر الرجوع للشاشة الرئيسية أثناء رحلة نشطة
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
if (pipEnabled) {
|
||||
enterPipMode()
|
||||
}
|
||||
}
|
||||
|
||||
// إعلام Flutter بتغيير وضع PiP
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPipMode: Boolean,
|
||||
newConfig: Configuration
|
||||
) {
|
||||
super.onPictureInPictureModeChanged(isInPipMode, newConfig)
|
||||
// يمكن لاحقاً إرسال حدث لـ Flutter لإخفاء/إظهار عناصر الواجهة
|
||||
flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger ->
|
||||
MethodChannel(messenger, PIP_CHANNEL)
|
||||
.invokeMethod("onPipChanged", isInPipMode)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- أمن الجهاز (كما عندك تقريباً) ----------------
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -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 {
|
||||
ext.kotlin_version = '2.1.0'
|
||||
repositories {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 54;
|
||||
objectVersion = 70;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -121,7 +121,7 @@
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
C663DBEB2F50907200D79908 /* Exceptions for "RideWidget" folder in "RideWidgetExtension" target */ = {
|
||||
C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
@@ -131,18 +131,7 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
C663DBD82F50907000D79908 /* RideWidget */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
C663DBEB2F50907200D79908 /* Exceptions for "RideWidget" folder in "RideWidgetExtension" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = RideWidget;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C663DBD82F50907000D79908 /* RideWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C663DBEB2F50907200D79908 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = RideWidget; sourceTree = "<group>"; };
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -480,10 +469,14 @@
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
|
||||
@@ -1,106 +1,106 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true />
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Intaleq</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Intaleq</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>32</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>intaleqapp.com</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
|
||||
<string>intaleq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.1.32</string>
|
||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||
<string>NO</string>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<true />
|
||||
<key>GMSApiKey</key>
|
||||
<string>YOUR_API_KEY</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>googlechromes</string>
|
||||
<string>comgooglemaps</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true />
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app requires access to your camera in order to scan QR codes and capture images
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Intaleq</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>Intaleq</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>33</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>intaleqapp.com</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.1086900987150-9jv4oa8l3t23d54lrf27c1d22tbt9i6d</string>
|
||||
<string>intaleq</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.1.33</string>
|
||||
<key>FirebaseAppDelegateProxyEnabled</key>
|
||||
<string>NO</string>
|
||||
<key>FlutterDeepLinkingEnabled</key>
|
||||
<true/>
|
||||
<key>GMSApiKey</key>
|
||||
<string>YOUR_API_KEY</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>googlechromes</string>
|
||||
<string>comgooglemaps</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app requires access to your camera in order to scan QR codes and capture images
|
||||
for uploading and access to connect to a call.</string>
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>This app requires contacts access to function properly.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Use Face ID to securely authenticate payment accounts.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>This app needs access to your location to provide you with the best ride experience.
|
||||
<key>NSContactsUsageDescription</key>
|
||||
<string>This app requires contacts access to function properly.</string>
|
||||
<key>NSFaceIDUsageDescription</key>
|
||||
<string>Use Face ID to securely authenticate payment accounts.</string>
|
||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||
<string>This app needs access to your location to provide you with the best ride experience.
|
||||
Your location data will be used to find the nearest available cars and connect you with
|
||||
the closest captain for efficient and convenient rides.</string>
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>This app needs access to location.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>This app needs access to your location to provide you with the best ride experience.
|
||||
<key>NSLocationAlwaysUsageDescription</key>
|
||||
<string>This app needs access to location.</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>This app needs access to your location to provide you with the best ride experience.
|
||||
Your location data will be used to find the nearest available cars and connect you with
|
||||
the closest captain for efficient and convenient rides.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app requires access to your microphone to record audio, allowing you to add
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>This app requires access to your microphone to record audio, allowing you to add
|
||||
voice recordings to your photos and videos and access to connect to a call.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app requires access to the photo library to upload pictures.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true />
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true />
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>location</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false />
|
||||
</dict>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app requires access to the photo library to upload pictures.</string>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>location</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:math';
|
||||
import 'package:Intaleq/constant/api_key.dart';
|
||||
import 'package:Intaleq/controller/firebase/firbase_messge.dart';
|
||||
import 'package:Intaleq/views/auth/otp_page.dart';
|
||||
import 'package:Intaleq/views/widgets/error_snakbar.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'package:Intaleq/constant/info.dart';
|
||||
@@ -276,37 +277,47 @@ class LoginController extends GetxController {
|
||||
// مهم: تأكد من passengerID في الـ box
|
||||
box.write(BoxName.passengerID, passengerID);
|
||||
|
||||
// 4) نفّذ عمليات مكلفة بالتوازي: getTokens + fingerprint
|
||||
// 4) تنفيذ العمليات بالتوازي: getTokens + fingerprint محلي
|
||||
final results = await Future.wait([
|
||||
CRUD().get(link: AppLink.getTokens, payload: {
|
||||
'passengerID': passengerID, // FIX: لا تستخدم box هنا
|
||||
}),
|
||||
CRUD().get(
|
||||
link: AppLink.getTokens, payload: {'passengerID': passengerID}),
|
||||
DeviceHelper.getDeviceFingerprint(),
|
||||
]);
|
||||
await box.write(BoxName.firstTimeLoadKey, 'false');
|
||||
final tokenResp = results[0];
|
||||
final fingerPrint = (results[1] ?? '').toString();
|
||||
await storage.write(key: BoxName.fingerPrint, value: fingerPrint);
|
||||
|
||||
final tokenResp = results[0];
|
||||
final localFP = (results[1] ?? '').toString();
|
||||
|
||||
await storage.write(key: BoxName.fingerPrint, value: localFP);
|
||||
await box.write(BoxName.firstTimeLoadKey, 'false');
|
||||
|
||||
// ── 5. المقارنة: FCM token + fingerprint ──────────────────────
|
||||
if (email != '962798583052@intaleqapp.com' && tokenResp != 'failure') {
|
||||
final tokenJson = jsonDecode(tokenResp);
|
||||
final serverToken = tokenJson['message']?['token']?.toString() ?? '';
|
||||
// Log.print('serverToken: ${serverToken}');
|
||||
final localFcm = (box.read(BoxName.tokenFCM) ?? '').toString();
|
||||
// Log.print('localFcm: ${localFcm}');
|
||||
final serverData = tokenJson['message'] as Map?; // null = أول تسجيل
|
||||
|
||||
// 5) اختلاف الجهاز -> تحقّق OTP
|
||||
if (serverToken.isNotEmpty && serverToken != localFcm) {
|
||||
final goVerify = await _confirmDeviceChangeDialog();
|
||||
if (goVerify == true) {
|
||||
if (serverData != null) {
|
||||
final serverFCM = serverData['token']?.toString() ?? '';
|
||||
final serverFP = serverData['fingerPrint']?.toString() ?? '';
|
||||
|
||||
final localFCM = (box.read(BoxName.tokenFCM) ?? '').toString();
|
||||
|
||||
// ── اختلاف أي منهما = جهاز مختلف أو تثبيت جديد ─────────
|
||||
final fcmChanged = serverFCM.isNotEmpty && serverFCM != localFCM;
|
||||
final fpChanged = serverFP.isNotEmpty && serverFP != localFP;
|
||||
|
||||
if (fcmChanged || fpChanged) {
|
||||
// final goVerify = await _confirmDeviceChangeDialog();
|
||||
// if (goVerify == true) {
|
||||
mySnackbarInfo('Device Change Detected'.tr);
|
||||
//
|
||||
await Get.to(() => OtpVerificationPage(
|
||||
phone: data['phone'].toString(),
|
||||
deviceToken: fingerPrint,
|
||||
token: tokenResp.toString(),
|
||||
ptoken: serverToken,
|
||||
deviceToken: localFP,
|
||||
token: tokenResp,
|
||||
ptoken: serverFCM, // نمرر FCM القديم للـ OTP controller
|
||||
));
|
||||
// بعد العودة من OTP (نجح/فشل)، أخرج من الميثود كي لا يحصل offAll مرتين
|
||||
return;
|
||||
return; // لا تكمل — الـ OTP controller يتولى الانتقال
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,18 +370,18 @@ class LoginController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool?> _confirmDeviceChangeDialog() {
|
||||
return Get.defaultDialog<bool>(
|
||||
barrierDismissible: false,
|
||||
title: 'Device Change Detected'.tr,
|
||||
middleText: 'Please verify your identity'.tr,
|
||||
textConfirm: 'Verify'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () => Get.back(result: true),
|
||||
textCancel: 'Cancel'.tr,
|
||||
onCancel: () => Get.back(result: false),
|
||||
);
|
||||
}
|
||||
// Future<bool?> _confirmDeviceChangeDialog() {
|
||||
// return Get.defaultDialog<bool>(
|
||||
// barrierDismissible: false,
|
||||
// title: 'Device Change Detected'.tr,
|
||||
// middleText: 'Please verify your identity'.tr,
|
||||
// textConfirm: 'Verify'.tr,
|
||||
// confirmTextColor: Colors.white,
|
||||
// onConfirm: () => Get.back(result: true),
|
||||
// textCancel: 'Cancel'.tr,
|
||||
// onCancel: () => Get.back(result: false),
|
||||
// );
|
||||
// }
|
||||
|
||||
void login() async {
|
||||
isloading = true;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
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