Fixes & Updates - 2026-06-01: Integrate Back-End v3 updates, fix call/connection issues across apps
This commit is contained in:
@@ -97,6 +97,8 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation "androidx.car.app:app:1.4.0"
|
||||
implementation "org.maplibre.gl:android-sdk:11.0.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
// تمت الترقية لتطابق تطبيق الراكب
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
|
||||
14
android/app/proguard-rules.pro
vendored
14
android/app/proguard-rules.pro
vendored
@@ -35,4 +35,16 @@
|
||||
#-keep class com.sefer_driver.RootDetection { *; }
|
||||
-keep class com.sefer_driver.RootDetection {
|
||||
native <methods>;
|
||||
}
|
||||
}
|
||||
|
||||
# Android Auto Car App Library
|
||||
-keep class androidx.car.app.** { *; }
|
||||
-keep class com.intaleq_driver.MyCarAppService { *; }
|
||||
-keep class com.intaleq_driver.MyCarSession { *; }
|
||||
-keep class com.intaleq_driver.MyCarScreen { *; }
|
||||
-keep class com.intaleq_driver.CarNavigationData { *; }
|
||||
-keep class com.intaleq_driver.MapPresentation { *; }
|
||||
|
||||
# MapLibre Native SDK
|
||||
-keep class org.maplibre.android.** { *; }
|
||||
-dontwarn org.maplibre.android.**
|
||||
@@ -70,6 +70,27 @@
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="intaleqapp" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Navigation Intents -->
|
||||
<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="geo" />
|
||||
</intent-filter>
|
||||
|
||||
<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="google.navigation" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="vnd.android.cursor.item/map" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- أنشطة ومكوّنات إضافية -->
|
||||
<activity android:name="com.yalantis.ucrop.UCropActivity" android:screenOrientation="portrait"
|
||||
@@ -146,6 +167,22 @@
|
||||
</receiver>
|
||||
<!-- مستقبل برودكاست خاص بك -->
|
||||
<receiver android:name=".YourBroadcastReceiver" android:exported="false" />
|
||||
|
||||
<!-- Android Auto Support -->
|
||||
<meta-data
|
||||
android:name="androidx.car.app.minCarAppApiLevel"
|
||||
android:value="1" />
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
android:resource="@xml/automotive_app_desc" />
|
||||
<service
|
||||
android:name=".MyCarAppService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.car.app.CarAppService" />
|
||||
<category android:name="androidx.car.app.category.NAVIGATION" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
</manifest>
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.intaleq_driver
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
|
||||
/**
|
||||
* كائن مشترك (Singleton) يحمل بيانات التوجيه في الوقت الحقيقي.
|
||||
* يتم تحديثه من فلاتر عبر MethodChannel في MainActivity،
|
||||
* ويتم قراءته من MyCarScreen و MapPresentation لعرض البيانات على شاشة السيارة.
|
||||
*/
|
||||
object CarNavigationData {
|
||||
// --- بيانات الموقع ---
|
||||
var currentLat: Double = 0.0
|
||||
var currentLng: Double = 0.0
|
||||
var currentBearing: Double = 0.0
|
||||
|
||||
// --- بيانات التوجيه ---
|
||||
var currentInstruction: String = "في انتظار بدء الرحلة..."
|
||||
var distanceToNextStepMeters: Double = 0.0
|
||||
var totalDistanceRemainingMeters: Double = 0.0
|
||||
var estimatedTimeRemainingSeconds: Double = 0.0
|
||||
var maneuverType: Int = 0 // يطابق قيم Maneuver.TYPE_* من Car App Library
|
||||
|
||||
// --- حالة التوجيه ---
|
||||
var isNavigating: Boolean = false
|
||||
var currentSpeed: Double = 0.0 // km/h
|
||||
|
||||
// --- نظام المستمعين ---
|
||||
private val listeners = mutableListOf<() -> Unit>()
|
||||
|
||||
fun addListener(listener: () -> Unit) {
|
||||
listeners.add(listener)
|
||||
}
|
||||
|
||||
fun removeListener(listener: () -> Unit) {
|
||||
listeners.remove(listener)
|
||||
}
|
||||
|
||||
fun notifyListeners() {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
listeners.forEach { it.invoke() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,50 @@ class MainActivity : FlutterFragmentActivity() {
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Channel for Android Auto Navigation Updates
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.intaleq_driver/car_navigation")
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"updateNavState" -> {
|
||||
// تحديث شامل لجميع بيانات التوجيه دفعة واحدة
|
||||
CarNavigationData.currentLat = call.argument<Double>("lat") ?: CarNavigationData.currentLat
|
||||
CarNavigationData.currentLng = call.argument<Double>("lng") ?: CarNavigationData.currentLng
|
||||
CarNavigationData.currentBearing = call.argument<Double>("bearing") ?: CarNavigationData.currentBearing
|
||||
CarNavigationData.currentSpeed = call.argument<Double>("speed") ?: CarNavigationData.currentSpeed
|
||||
CarNavigationData.currentInstruction = call.argument<String>("instruction") ?: CarNavigationData.currentInstruction
|
||||
CarNavigationData.distanceToNextStepMeters = call.argument<Double>("distanceToStep") ?: CarNavigationData.distanceToNextStepMeters
|
||||
CarNavigationData.totalDistanceRemainingMeters = call.argument<Double>("totalDistance") ?: CarNavigationData.totalDistanceRemainingMeters
|
||||
CarNavigationData.estimatedTimeRemainingSeconds = call.argument<Double>("eta") ?: CarNavigationData.estimatedTimeRemainingSeconds
|
||||
CarNavigationData.maneuverType = call.argument<Int>("maneuver") ?: CarNavigationData.maneuverType
|
||||
CarNavigationData.isNavigating = call.argument<Boolean>("isNavigating") ?: CarNavigationData.isNavigating
|
||||
CarNavigationData.notifyListeners()
|
||||
result.success(true)
|
||||
}
|
||||
"updateLocation" -> {
|
||||
CarNavigationData.currentLat = call.argument<Double>("lat") ?: 0.0
|
||||
CarNavigationData.currentLng = call.argument<Double>("lng") ?: 0.0
|
||||
CarNavigationData.currentBearing = call.argument<Double>("bearing") ?: CarNavigationData.currentBearing
|
||||
CarNavigationData.currentSpeed = call.argument<Double>("speed") ?: CarNavigationData.currentSpeed
|
||||
CarNavigationData.notifyListeners()
|
||||
result.success(true)
|
||||
}
|
||||
"updateInstruction" -> {
|
||||
CarNavigationData.currentInstruction = call.argument<String>("instruction") ?: ""
|
||||
CarNavigationData.maneuverType = call.argument<Int>("maneuver") ?: 0
|
||||
CarNavigationData.distanceToNextStepMeters = call.argument<Double>("distanceToStep") ?: 0.0
|
||||
CarNavigationData.notifyListeners()
|
||||
result.success(true)
|
||||
}
|
||||
"stopNavigation" -> {
|
||||
CarNavigationData.isNavigating = false
|
||||
CarNavigationData.currentInstruction = "تمت الرحلة بنجاح!"
|
||||
CarNavigationData.notifyListeners()
|
||||
result.success(true)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.intaleq_driver
|
||||
|
||||
import android.app.Presentation
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.Display
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import org.maplibre.android.MapLibre
|
||||
import org.maplibre.android.camera.CameraPosition
|
||||
import org.maplibre.android.camera.CameraUpdateFactory
|
||||
import org.maplibre.android.geometry.LatLng
|
||||
import org.maplibre.android.maps.MapView
|
||||
import org.maplibre.android.maps.MapLibreMap
|
||||
import org.maplibre.android.maps.Style
|
||||
|
||||
/**
|
||||
* شاشة عرض وهمية (Presentation) تُرسم على VirtualDisplay الخاص بشاشة السيارة.
|
||||
* تستخدم MapLibre Native SDK لعرض الخريطة بنفس ستايل تطبيق انطلق.
|
||||
*/
|
||||
class MapPresentation(outerContext: Context, display: Display) : Presentation(outerContext, display) {
|
||||
lateinit var mapView: MapView
|
||||
private var mapboxMap: MapLibreMap? = null
|
||||
private var isMapReady = false
|
||||
|
||||
private val locationListener: () -> Unit = {
|
||||
updateCameraPosition()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
MapLibre.getInstance(context)
|
||||
|
||||
val root = FrameLayout(context)
|
||||
root.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
mapView = MapView(context)
|
||||
mapView.layoutParams = FrameLayout.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.MATCH_PARENT
|
||||
)
|
||||
|
||||
root.addView(mapView)
|
||||
setContentView(root)
|
||||
|
||||
mapView.onCreate(savedInstanceState)
|
||||
mapView.getMapAsync { map ->
|
||||
mapboxMap = map
|
||||
|
||||
// تحميل ستايل خرائط انطلق من أصول فلاتر
|
||||
val styleUrl = "asset://flutter_assets/assets/style.json"
|
||||
map.setStyle(Style.Builder().fromUri(styleUrl)) {
|
||||
isMapReady = true
|
||||
updateCameraPosition()
|
||||
}
|
||||
|
||||
// إعدادات مناسبة لشاشة السيارة
|
||||
map.uiSettings.isCompassEnabled = false
|
||||
map.uiSettings.isLogoEnabled = false
|
||||
map.uiSettings.isAttributionEnabled = false
|
||||
}
|
||||
|
||||
// الاستماع لتحديثات الموقع القادمة من فلاتر
|
||||
CarNavigationData.addListener(locationListener)
|
||||
}
|
||||
|
||||
private fun updateCameraPosition() {
|
||||
if (!isMapReady || mapboxMap == null) return
|
||||
|
||||
val lat = CarNavigationData.currentLat
|
||||
val lng = CarNavigationData.currentLng
|
||||
val bearing = CarNavigationData.currentBearing
|
||||
val speed = CarNavigationData.currentSpeed
|
||||
|
||||
if (lat == 0.0 && lng == 0.0) return
|
||||
|
||||
// حساب الزوم المناسب بناءً على السرعة (نفس المنطق في فلاتر)
|
||||
val zoom = when {
|
||||
speed < 15 -> 17.0
|
||||
speed < 40 -> 16.5
|
||||
speed < 70 -> 15.5
|
||||
speed < 100 -> 15.0
|
||||
else -> 14.0
|
||||
}
|
||||
|
||||
// حساب الميل (Tilt) بناءً على السرعة لتأثير ثلاثي الأبعاد
|
||||
val tilt = when {
|
||||
speed < 10 -> 0.0
|
||||
speed < 40 -> 40.0
|
||||
else -> 55.0
|
||||
}
|
||||
|
||||
val position = CameraPosition.Builder()
|
||||
.target(LatLng(lat, lng))
|
||||
.zoom(zoom)
|
||||
.bearing(bearing)
|
||||
.tilt(tilt)
|
||||
.build()
|
||||
|
||||
mapboxMap?.animateCamera(
|
||||
CameraUpdateFactory.newCameraPosition(position),
|
||||
1000 // انتقال سلس خلال ثانية
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStart() { super.onStart(); mapView.onStart() }
|
||||
override fun onStop() { super.onStop(); mapView.onStop() }
|
||||
|
||||
fun onResume() { mapView.onResume() }
|
||||
fun onPause() { mapView.onPause() }
|
||||
fun onDestroy() {
|
||||
CarNavigationData.removeListener(locationListener)
|
||||
mapView.onDestroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.intaleq_driver
|
||||
|
||||
import android.content.pm.ApplicationInfo
|
||||
import androidx.car.app.CarAppService
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.validation.HostValidator
|
||||
|
||||
class MyCarAppService : CarAppService() {
|
||||
override fun createHostValidator(): HostValidator {
|
||||
// في وضع التطوير: نسمح لجميع المستضيفين (DHU + أي تطبيق)
|
||||
// في وضع الإنتاج: نسمح فقط لتطبيقات Android Auto و Google الرسمية
|
||||
return if (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) {
|
||||
HostValidator.ALLOW_ALL_HOSTS_VALIDATOR
|
||||
} else {
|
||||
HostValidator.Builder(applicationContext)
|
||||
.addAllowedHosts(androidx.car.app.R.array.hosts_allowlist_sample)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateSession(): Session {
|
||||
return MyCarSession()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.intaleq_driver
|
||||
|
||||
import androidx.car.app.CarContext
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.model.Action
|
||||
import androidx.car.app.model.CarColor
|
||||
import androidx.car.app.model.CarIcon
|
||||
import androidx.car.app.model.Distance
|
||||
import androidx.car.app.model.MessageTemplate
|
||||
import androidx.car.app.model.Template
|
||||
import androidx.car.app.navigation.model.Maneuver
|
||||
import androidx.car.app.navigation.model.NavigationTemplate
|
||||
import androidx.car.app.navigation.model.RoutingInfo
|
||||
import androidx.car.app.navigation.model.Step
|
||||
|
||||
class MyCarScreen(carContext: CarContext) : Screen(carContext) {
|
||||
|
||||
init {
|
||||
CarNavigationData.addListener {
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGetTemplate(): Template {
|
||||
// إذا لم يكن التوجيه نشطاً بعد، نعرض شاشة ترحيبية
|
||||
if (!CarNavigationData.isNavigating) {
|
||||
return MessageTemplate.Builder("مرحباً بك في انطلق درايفر\nبانتظار بدء رحلة جديدة...")
|
||||
.setTitle("Intaleq Driver")
|
||||
.setHeaderAction(Action.APP_ICON)
|
||||
.build()
|
||||
}
|
||||
|
||||
// --- بناء معلومات التوجيه (Turn-by-Turn) ---
|
||||
val maneuverType = mapIntaleqManeuverToCarManeuver(CarNavigationData.maneuverType)
|
||||
val maneuver = Maneuver.Builder(maneuverType).build()
|
||||
|
||||
val step = Step.Builder(CarNavigationData.currentInstruction)
|
||||
.setManeuver(maneuver)
|
||||
.build()
|
||||
|
||||
val distanceToStep = Distance.create(
|
||||
CarNavigationData.distanceToNextStepMeters,
|
||||
Distance.UNIT_METERS
|
||||
)
|
||||
|
||||
val routingInfo = RoutingInfo.Builder()
|
||||
.setCurrentStep(step, distanceToStep)
|
||||
.build()
|
||||
|
||||
// --- بناء قالب التوجيه ---
|
||||
return NavigationTemplate.Builder()
|
||||
.setNavigationInfo(routingInfo)
|
||||
.setActionStrip(
|
||||
androidx.car.app.model.ActionStrip.Builder()
|
||||
.addAction(Action.APP_ICON)
|
||||
.build()
|
||||
)
|
||||
.setBackgroundColor(CarColor.PRIMARY)
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* تحويل أكواد الانعطاف الخاصة بتطبيق انطلق (NavigationController.currentManeuverModifier)
|
||||
* إلى أكواد Maneuver الرسمية من مكتبة Android for Cars.
|
||||
*/
|
||||
private fun mapIntaleqManeuverToCarManeuver(intaleqCode: Int): Int {
|
||||
return when (intaleqCode) {
|
||||
0 -> Maneuver.TYPE_STRAIGHT // مستقيم
|
||||
2 -> Maneuver.TYPE_TURN_NORMAL_RIGHT // يمين
|
||||
3 -> Maneuver.TYPE_TURN_SLIGHT_RIGHT // يمين خفيف
|
||||
-2 -> Maneuver.TYPE_TURN_NORMAL_LEFT // يسار
|
||||
-1 -> Maneuver.TYPE_TURN_SLIGHT_LEFT // يسار خفيف
|
||||
4 -> Maneuver.TYPE_DESTINATION // وصلت
|
||||
6 -> Maneuver.TYPE_ROUNDABOUT_ENTER_AND_EXIT_CW // دوار
|
||||
7 -> Maneuver.TYPE_KEEP_RIGHT // ابق يمين
|
||||
-7 -> Maneuver.TYPE_KEEP_LEFT // ابق يسار
|
||||
else -> Maneuver.TYPE_UNKNOWN
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.intaleq_driver
|
||||
|
||||
import android.content.Intent
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.hardware.display.VirtualDisplay
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.car.app.AppManager
|
||||
import androidx.car.app.Screen
|
||||
import androidx.car.app.Session
|
||||
import androidx.car.app.SurfaceCallback
|
||||
import androidx.car.app.SurfaceContainer
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
|
||||
class MyCarSession : Session(), DefaultLifecycleObserver {
|
||||
private var virtualDisplay: VirtualDisplay? = null
|
||||
private var presentation: MapPresentation? = null
|
||||
|
||||
override fun onCreateScreen(intent: Intent): Screen {
|
||||
lifecycle.addObserver(this)
|
||||
|
||||
val appManager = carContext.getCarService(AppManager::class.java)
|
||||
appManager.setSurfaceCallback(object : SurfaceCallback {
|
||||
override fun onSurfaceAvailable(surfaceContainer: SurfaceContainer) {
|
||||
val surface = surfaceContainer.surface ?: return
|
||||
val width = surfaceContainer.width
|
||||
val height = surfaceContainer.height
|
||||
val dpi = surfaceContainer.dpi
|
||||
|
||||
val displayManager = carContext.getSystemService(DisplayManager::class.java)
|
||||
virtualDisplay = displayManager.createVirtualDisplay(
|
||||
"CarAppMapDisplay",
|
||||
width,
|
||||
height,
|
||||
dpi,
|
||||
surface,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||
)
|
||||
|
||||
virtualDisplay?.display?.let { display ->
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
presentation = MapPresentation(carContext, display)
|
||||
presentation?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onVisibleAreaChanged(visibleArea: android.graphics.Rect) {
|
||||
// تحديث المساحة المرئية إذا لزم الأمر
|
||||
}
|
||||
|
||||
override fun onSurfaceDestroyed(surfaceContainer: SurfaceContainer) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
presentation?.dismiss()
|
||||
presentation = null
|
||||
}
|
||||
virtualDisplay?.release()
|
||||
virtualDisplay = null
|
||||
}
|
||||
})
|
||||
|
||||
return MyCarScreen(carContext)
|
||||
}
|
||||
|
||||
override fun onResume(owner: LifecycleOwner) { presentation?.onResume() }
|
||||
override fun onPause(owner: LifecycleOwner) { presentation?.onPause() }
|
||||
override fun onDestroy(owner: LifecycleOwner) { presentation?.onDestroy() }
|
||||
}
|
||||
4
android/app/src/main/res/xml/automotive_app_desc.xml
Normal file
4
android/app/src/main/res/xml/automotive_app_desc.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<automotiveApp>
|
||||
<uses name="navigation" />
|
||||
</automotiveApp>
|
||||
Reference in New Issue
Block a user