2026-02-28-1
This commit is contained in:
@@ -100,4 +100,5 @@ dependencies {
|
||||
implementation 'com.stripe:paymentsheet:21.4.2'
|
||||
implementation 'com.scottyab:rootbeer-lib:0.1.0'
|
||||
implementation 'com.google.android.gms:play-services-safetynet:18.1.0'
|
||||
implementation 'com.google.android.gms:play-services-location:21.3.0'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.your.package.name">
|
||||
package="com.Intaleq.intaleq">
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -14,6 +14,9 @@
|
||||
<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" />
|
||||
@@ -27,6 +30,11 @@
|
||||
android:usesCleartextTraffic="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!-- ✅ مهم جداً: تعريف أن المشروع يستخدم V2 Embedding -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -47,7 +55,7 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- ✅ App Links (Verified Domain Only) -->
|
||||
<!-- 🔗 App Links -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -64,7 +72,7 @@
|
||||
android:pathPrefix="/" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- ✅ Custom Scheme -->
|
||||
<!-- 🔗 Custom Scheme -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
@@ -98,7 +106,11 @@
|
||||
<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"
|
||||
|
||||
@@ -17,35 +17,89 @@ import kotlin.concurrent.schedule
|
||||
|
||||
class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
// The channel name is updated to use your new package name.
|
||||
private val CHANNEL_NAME = "com.Intaleq.intaleq/security"
|
||||
private lateinit var channel: MethodChannel
|
||||
// قناة الأمان (كما هي)
|
||||
private val SECURITY_CHANNEL_NAME = "com.Intaleq.intaleq/security"
|
||||
private lateinit var securityChannel: MethodChannel
|
||||
|
||||
// قناة تتبّع الرحلة (Live Activity على أندرويد)
|
||||
private val RIDE_TRACKING_CHANNEL = "intaleq/ride_tracking"
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
// Initialize the MethodChannel with the new name
|
||||
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL_NAME)
|
||||
// -------- 1) قناة الأمان --------
|
||||
securityChannel =
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, SECURITY_CHANNEL_NAME)
|
||||
|
||||
// Set a MethodCallHandler to handle method calls from Flutter
|
||||
channel.setMethodCallHandler { call, result ->
|
||||
securityChannel.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"isNativeRooted" -> {
|
||||
val isCompromised = isDeviceCompromised()
|
||||
result.success(isCompromised) // Send the result back to Flutter
|
||||
result.success(isCompromised)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented() // Handle unknown method calls
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------- 2) قناة تتبع الرحلة (Ride Tracking) --------
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, RIDE_TRACKING_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)
|
||||
}
|
||||
"stopRideTracking" -> {
|
||||
RideTrackingService.stop(this)
|
||||
result.success(null)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- أمن الجهاز (كما عندك تقريباً) ----------------
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Perform all checks. The order can matter; you might want to prioritize
|
||||
// the faster checks first.
|
||||
if (isDeviceCompromised()) {
|
||||
showSecurityWarningDialog()
|
||||
}
|
||||
@@ -53,24 +107,22 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
private fun isDeviceCompromised(): Boolean {
|
||||
return try {
|
||||
// NOTE: The SafetyNet check is commented out, just like in your old code.
|
||||
nativeRootCheck() || rootBeerCheck() // || !safetyNetCheck()
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecurityCheck", "Error during security checks: ${e.message}", e)
|
||||
true // Consider the device compromised on error
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun nativeRootCheck(): Boolean {
|
||||
Log.d("SecurityCheck", "Starting native root detection...")
|
||||
return try {
|
||||
// This assumes you have a 'RootDetection' object/class in your project.
|
||||
val isNativeRooted = RootDetection.isNativeRooted()
|
||||
Log.d("SecurityCheck", "Native root detection result: $isNativeRooted")
|
||||
isNativeRooted
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecurityCheck", "Error in native root detection: ${e.message}", e)
|
||||
true // Consider rooted on exception
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,38 +134,36 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
return isRooted
|
||||
}
|
||||
|
||||
// IMPORTANT: The SafetyNet API is deprecated. It's recommended to migrate to the Play Integrity
|
||||
// API.
|
||||
// This function is kept here from your old code for reference.
|
||||
// SafetyNet (معلّق كما هو)
|
||||
private fun safetyNetCheck(): Boolean {
|
||||
Log.d("SecurityCheck", "Starting SafetyNet check...")
|
||||
var isSafe = false // Initialize a variable to store result
|
||||
val semaphore = java.util.concurrent.Semaphore(0) // Create a semaphore
|
||||
var isSafe = false
|
||||
val semaphore = java.util.concurrent.Semaphore(0)
|
||||
|
||||
SafetyNetCheck.checkSafetyNet(this, getString(R.string.api_key_safety)) { result ->
|
||||
isSafe = result
|
||||
Log.d("SecurityCheck", "SafetyNet check result: $isSafe")
|
||||
semaphore.release() // Release the semaphore when the callback is executed
|
||||
semaphore.release()
|
||||
}
|
||||
|
||||
try {
|
||||
semaphore.acquire() // Wait for the callback to complete
|
||||
semaphore.acquire()
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e("SecurityCheck", "Interrupted while waiting for SafetyNet check", e)
|
||||
return false // Or handle as appropriate for your app
|
||||
return false
|
||||
}
|
||||
return isSafe
|
||||
}
|
||||
|
||||
private fun showSecurityWarningDialog() {
|
||||
var secondsRemaining = 10 // Start at 10 seconds
|
||||
var secondsRemaining = 10
|
||||
|
||||
// Create views programmatically
|
||||
val progressBar = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal)
|
||||
progressBar.max = 10 // Set max to 10 for 10 seconds
|
||||
progressBar.progress = 10 // Start full
|
||||
progressBar.max = 10
|
||||
progressBar.progress = 10
|
||||
|
||||
val textView = TextView(this)
|
||||
textView.text = getString(R.string.security_warning_message) // Your message
|
||||
textView.text = getString(R.string.security_warning_message)
|
||||
textView.textAlignment = TextView.TEXT_ALIGNMENT_CENTER
|
||||
|
||||
val layout = LinearLayout(this)
|
||||
@@ -124,26 +174,22 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
val dialog =
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.security_warning_title)) // Your title
|
||||
.setView(layout) // Set the custom layout
|
||||
.setCancelable(
|
||||
false
|
||||
) // Prevent dismissing by tapping outside or back button
|
||||
.setTitle(getString(R.string.security_warning_title))
|
||||
.setView(layout)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
|
||||
dialog.show()
|
||||
|
||||
// Use a Timer to update the progress bar and countdown
|
||||
val timer = Timer()
|
||||
timer.schedule(0, 1000) { // Update every 1000ms (1 second)
|
||||
timer.schedule(0, 1000) {
|
||||
secondsRemaining--
|
||||
runOnUiThread { // Update UI on the main thread
|
||||
progressBar.progress =
|
||||
secondsRemaining // Set the progress bar to show remaining seconds
|
||||
runOnUiThread {
|
||||
progressBar.progress = secondsRemaining
|
||||
if (secondsRemaining <= 0) {
|
||||
timer.cancel() // Stop the timer
|
||||
dialog.dismiss() // Dismiss the dialog
|
||||
clearAppDataAndExit() // Clear data and exit
|
||||
timer.cancel()
|
||||
dialog.dismiss()
|
||||
clearAppDataAndExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,19 +197,16 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
private fun clearAppDataAndExit() {
|
||||
try {
|
||||
// Note: This command often requires system permissions and may fail on user apps.
|
||||
// The fallback methods below will be used if it fails.
|
||||
val packageName = applicationContext.packageName
|
||||
val runtime = Runtime.getRuntime()
|
||||
runtime.exec("pm clear $packageName") // Clear app data
|
||||
runtime.exec("pm clear $packageName")
|
||||
} catch (e: Exception) {
|
||||
// Fallback to manual deletion if 'pm clear' fails
|
||||
clearCache()
|
||||
clearAppData()
|
||||
}
|
||||
|
||||
finishAffinity() // Finish all activities from this app
|
||||
System.exit(0) // Terminate the app's process
|
||||
finishAffinity()
|
||||
System.exit(0)
|
||||
}
|
||||
|
||||
private fun clearCache() {
|
||||
@@ -177,7 +220,6 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
|
||||
private fun clearAppData() {
|
||||
try {
|
||||
// Be careful with this, as it deletes everything in the app's data directory.
|
||||
deleteDir(applicationContext.dataDir)
|
||||
} catch (e: Exception) {
|
||||
Log.e("SecurityCheck", "Error clearing app data: ${e.message}", e)
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
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
|
||||
}
|
||||
BIN
android/app/src/main/res/drawable/car_icon.png
Normal file
BIN
android/app/src/main/res/drawable/car_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
18
android/app/src/main/res/drawable/road_bg.xml
Normal file
18
android/app/src/main/res/drawable/road_bg.xml
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="#2C2C2C" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
<item android:top="15dp" android:bottom="15dp">
|
||||
<shape android:shape="line">
|
||||
<stroke
|
||||
android:color="#FFFFFF"
|
||||
android:width="2dp"
|
||||
android:dashWidth="10dp"
|
||||
android:dashGap="10dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
157
android/app/src/main/res/layout/notification_ride_live.xml
Normal file
157
android/app/src/main/res/layout/notification_ride_live.xml
Normal file
@@ -0,0 +1,157 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user