Update: 2026-07-04 16:16:49

This commit is contained in:
Hamza-Ayed
2026-07-04 16:16:50 +03:00
parent 8ae6a2e2da
commit c8fdd1bb5a
9 changed files with 576 additions and 0 deletions
@@ -68,6 +68,10 @@
<meta-data android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />
<meta-data android:name="io.flutter.embedding.android.EnableImpeller" android:value="false" />
<!-- Android Auto Metadata -->
<meta-data
android:name="com.android.car.meta"
android:resource="@xml/automotive_app_desc" />
<!-- Main Activity -->
<activity android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -134,6 +138,14 @@
<service android:name=".MyFirebaseMessagingService" android:exported="false" />
<service android:name=".LocationUpdatesService" android:exported="false"
android:foregroundServiceType="location" />
<!-- Android Auto Car App Service -->
<service
android:name=".MyCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
</intent-filter>
</service>
<!-- خدمة Firebase الرسمية لاستقبال رسائل FCM -->
<service android:name="com.google.firebase.messaging.FirebaseMessagingService"
android:exported="false" tools:replace="android:exported">
+120
View File
@@ -1,6 +1,7 @@
import UIKit
import Flutter
import FirebaseCore
import CarPlay
import CoreLocation
@@ -28,6 +29,7 @@ import CoreLocation
// تهيئة قنوات الاتصال مع Flutter
setupMethodChannel()
setupCarNavigationChannel()
// ضبط مدير الموقع لمتابعة تغييرات الأذونات
locationManager.delegate = self
@@ -36,6 +38,34 @@ import CoreLocation
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// MARK: - CarPlay Connection (Non-Scene API)
override func application(
_ application: UIApplication,
didConnectCarInterfaceController interfaceController: CPInterfaceController,
to window: CPWindow
) {
let sceneDelegate = CarPlaySceneDelegate()
sceneDelegate.interfaceController = interfaceController
let mapTemplate = CPMapTemplate()
sceneDelegate.mapTemplate = mapTemplate
mapTemplate.guidanceBackgroundStyle = .light
interfaceController.setRootTemplate(mapTemplate, animated: true)
CarPlayState.shared.sceneDelegate = sceneDelegate
}
override func application(
_ application: UIApplication,
didDisconnectCarInterfaceController interfaceController: CPInterfaceController,
from window: CPWindow
) {
CarPlayState.shared.sceneDelegate?.stopNavigation()
CarPlayState.shared.sceneDelegate = nil
}
// MARK: - تهيئة القناة بين Swift و Flutter
func setupMethodChannel() {
guard let controller = window?.rootViewController as? FlutterViewController else {
@@ -54,6 +84,96 @@ import CoreLocation
}
}
// MARK: - Car Navigation Method Channel
func setupCarNavigationChannel() {
guard let controller = window?.rootViewController as? FlutterViewController else {
return
}
let channel = FlutterMethodChannel(
name: "com.siro.siro_driver/car_navigation",
binaryMessenger: controller.binaryMessenger
)
channel.setMethodCallHandler { [weak self] call, result in
guard let self = self else { return }
switch call.method {
case "updateNavState":
let lat = (call.arguments as? [String: Any])?["lat"] as? Double ?? 0
let lng = (call.arguments as? [String: Any])?["lng"] as? Double ?? 0
let bearing = (call.arguments as? [String: Any])?["bearing"] as? Double ?? 0
let speed = (call.arguments as? [String: Any])?["speed"] as? Double ?? 0
let instruction = (call.arguments as? [String: Any])?["instruction"] as? String ?? ""
let distanceToStep = (call.arguments as? [String: Any])?["distanceToStep"] as? Double ?? 0
let eta = (call.arguments as? [String: Any])?["eta"] as? Double ?? 0
let maneuver = (call.arguments as? [String: Any])?["maneuver"] as? Int ?? 0
let isNavigating = (call.arguments as? [String: Any])?["isNavigating"] as? Bool ?? false
DispatchQueue.main.async {
guard let delegate = CarPlayState.shared.sceneDelegate else { return }
if isNavigating {
if delegate.navigationSession == nil {
delegate.startNavigation(
instruction: instruction,
distance: distanceToStep,
eta: eta,
maneuverType: maneuver
)
} else {
delegate.updateManeuver(
instruction: instruction,
distance: distanceToStep,
maneuverType: maneuver
)
delegate.updateArrivalEstimates(eta: eta)
}
delegate.updateLocation(lat: lat, lng: lng, bearing: bearing)
} else {
delegate.stopNavigation()
}
}
result(true)
case "updateLocation":
let lat = (call.arguments as? [String: Any])?["lat"] as? Double ?? 0
let lng = (call.arguments as? [String: Any])?["lng"] as? Double ?? 0
let bearing = (call.arguments as? [String: Any])?["bearing"] as? Double ?? 0
DispatchQueue.main.async {
CarPlayState.shared.sceneDelegate?.updateLocation(
lat: lat,
lng: lng,
bearing: bearing
)
}
result(true)
case "updateInstruction":
let instruction = (call.arguments as? [String: Any])?["instruction"] as? String ?? ""
let maneuver = (call.arguments as? [String: Any])?["maneuver"] as? Int ?? 0
let distanceToStep = (call.arguments as? [String: Any])?["distanceToStep"] as? Double ?? 0
DispatchQueue.main.async {
CarPlayState.shared.sceneDelegate?.updateManeuver(
instruction: instruction,
distance: distanceToStep,
maneuverType: maneuver
)
}
result(true)
case "stopNavigation":
DispatchQueue.main.async {
CarPlayState.shared.sceneDelegate?.stopNavigation()
}
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
}
// MARK: - عرض تنبيه عند اكتشاف جهاز مخترق
func showSecurityAlert() {
guard let rootVC = UIApplication.shared.keyWindow?.rootViewController else {
@@ -0,0 +1,97 @@
import CarPlay
import MapKit
class CarPlaySceneDelegate: NSObject {
var interfaceController: CPInterfaceController?
var mapTemplate: CPMapTemplate?
var navigationSession: CPNavigationSession?
// MARK: - Navigation Actions
func startNavigation(
instruction: String,
distance: Double,
eta: Double,
maneuverType: Int
) {
guard let mapTemplate = mapTemplate else { return }
let origin = MKMapItem(
placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0))
)
let destination = MKMapItem(
placemark: MKPlacemark(coordinate: CLLocationCoordinate2D(latitude: 0, longitude: 0))
)
let trip = CPTrip(origin: origin, destination: destination)
let session = mapTemplate.startNavigationSession(for: trip)
self.navigationSession = session
updateManeuver(instruction: instruction, distance: distance, maneuverType: maneuverType)
updateArrivalEstimates(eta: eta)
}
func updateManeuver(instruction: String, distance: Double, maneuverType: Int) {
guard let session = navigationSession else { return }
let maneuver = CPManeuver()
maneuver.instructionVariants = [instruction]
if let color = maneuverColor(for: maneuverType) {
maneuver.symbolColor = color
}
let distanceEstimate = CPTravelEstimates(
distance: Measurement(value: distance, unit: UnitLength.meters),
timeRemaining: 0
)
session.updateManeuvers([maneuver], travelEstimates: distanceEstimate)
}
func updateArrivalEstimates(eta: Double) {
guard let session = navigationSession else { return }
let arrivalEstimate = CPTravelEstimates(
distance: Measurement(value: 0, unit: UnitLength.meters),
timeRemaining: eta
)
// `updatingArrival` is deprecated; for iOS 15+ use `updating(_:)` on the maneuver's arrivalEstimates.
// We keep this for broad compatibility.
session.updatingArrival(to: arrivalEstimate)
}
func updateLocation(lat: Double, lng: Double, bearing: Double) {
let center = CLLocationCoordinate2D(latitude: lat, longitude: lng)
let region = MKCoordinateRegion(
center: center,
span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
)
mapTemplate?.showPanningInterface(animated: true)
}
func stopNavigation() {
guard let session = navigationSession else { return }
session.cancel()
self.navigationSession = nil
mapTemplate?.hideTripPreviews()
}
// MARK: - Helpers
private func maneuverColor(for type: Int) -> UIColor? {
switch type {
case 4: return .systemGreen
case 2, -2: return .systemBlue
default: return nil
}
}
}
// MARK: - Shared State
class CarPlayState {
static let shared = CarPlayState()
weak var sceneDelegate: CarPlaySceneDelegate?
private init() {}
}
+14
View File
@@ -95,6 +95,20 @@
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs access to your photo library to allow you to upload and manage
images.</string>
<key>CPApplication</key>
<array>
<dict>
<key>CPApplicationDriverManeuver</key>
<dict>
<key>supportsAudio</key>
<false/>
<key>supportsCommunication</key>
<false/>
<key>supportsNavigation</key>
<true/>
</dict>
</dict>
</array>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@@ -4,5 +4,7 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.carplay-driving-task</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,88 @@
import 'package:flutter/services.dart';
class CarPlatformBridge {
CarPlatformBridge._();
static const _channel = MethodChannel('com.siro.siro_driver/car_navigation');
static bool _isInitialized = false;
static void ensureInitialized() {
if (_isInitialized) return;
_channel.setMethodCallHandler(_handleMethodCall);
_isInitialized = true;
}
static Future<dynamic> _handleMethodCall(MethodCall call) async {
switch (call.method) {
case 'isCarAppConnected':
return false;
default:
throw MissingPluginException();
}
}
static Future<void> updateNavState({
required double lat,
required double lng,
required double bearing,
required double speed,
required String instruction,
required double distanceToStep,
required double totalDistance,
required double eta,
required int maneuver,
required bool isNavigating,
}) async {
try {
await _channel.invokeMethod('updateNavState', {
'lat': lat,
'lng': lng,
'bearing': bearing,
'speed': speed,
'instruction': instruction,
'distanceToStep': distanceToStep,
'totalDistance': totalDistance,
'eta': eta,
'maneuver': maneuver,
'isNavigating': isNavigating,
});
} catch (_) {}
}
static Future<void> updateLocation({
required double lat,
required double lng,
required double bearing,
required double speed,
}) async {
try {
await _channel.invokeMethod('updateLocation', {
'lat': lat,
'lng': lng,
'bearing': bearing,
'speed': speed,
});
} catch (_) {}
}
static Future<void> updateInstruction({
required String instruction,
required int maneuver,
required double distanceToStep,
}) async {
try {
await _channel.invokeMethod('updateInstruction', {
'instruction': instruction,
'maneuver': maneuver,
'distanceToStep': distanceToStep,
});
} catch (_) {}
}
static Future<void> stopNavigation() async {
try {
await _channel.invokeMethod('stopNavigation');
} catch (_) {}
}
}
@@ -11,6 +11,7 @@ import 'package:intaleq_maps/intaleq_maps.dart';
import 'package:http/http.dart' as http;
import 'package:siro_driver/constant/box_name.dart';
import 'package:siro_driver/constant/links.dart';
import 'package:siro_driver/controller/car_platform_bridge.dart';
import 'package:siro_driver/controller/functions/crud.dart';
import 'package:siro_driver/controller/functions/tts.dart';
import 'package:siro_driver/controller/home/navigation/decode_polyline_isolate.dart';
@@ -90,6 +91,7 @@ class NavigationController extends GetxController
String nextInstruction = "";
int currentStepIndex = 0;
String distanceToNextStep = "";
double _distanceToNextStepMeters = 0.0;
String totalDistanceRemaining = "";
String estimatedTimeRemaining = "";
dynamic currentManeuverModifier = 0;
@@ -300,6 +302,7 @@ class NavigationController extends GetxController
@override
void onInit() {
super.onInit();
CarPlatformBridge.ensureInitialized();
_animController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 1000));
_animController!.addListener(() {
@@ -546,6 +549,9 @@ class NavigationController extends GetxController
_recomputeETA();
_checkOffRoute(newLoc);
}
_pushCarBridgeUpdate();
update();
} catch (e) {
Log.print("DEBUG: Error in _handleLocationUpdate: $e");
@@ -1133,6 +1139,8 @@ class NavigationController extends GetxController
bearing: _smoothedHeading, zoom: _targetZoom, tilt: _targetTilt);
}
_pushCarBridgeUpdate();
update();
}
@@ -1153,6 +1161,7 @@ class NavigationController extends GetxController
_finalDestination = null;
isNavigating = false;
routes = [];
CarPlatformBridge.stopNavigation();
await _flushBufferToServer();
}
routeSteps = [];
@@ -1162,6 +1171,7 @@ class NavigationController extends GetxController
nextInstruction = "";
currentManeuverModifier = "siro";
distanceToNextStep = "";
_distanceToNextStepMeters = 0.0;
totalDistanceRemaining = "";
estimatedTimeRemaining = "";
arrivalTime = "--:--";
@@ -1197,6 +1207,7 @@ class NavigationController extends GetxController
final distance = Geolocator.distanceBetween(
pos.latitude, pos.longitude, endLatLng.latitude, endLatLng.longitude);
_distanceToNextStepMeters = distance;
distanceToNextStep = distance > 1000
? "${(distance / 1000).toStringAsFixed(1)} km"
: "${distance.toStringAsFixed(0)} m";
@@ -1224,6 +1235,13 @@ class NavigationController extends GetxController
: "Then ${routeSteps[currentStepIndex + 1]['text']}")
: (langCode == 'ar' ? "ستصل إلى وجهتك" : "Arriving soon");
_nextInstructionSpoken = false;
CarPlatformBridge.updateInstruction(
instruction: currentInstruction,
maneuver: currentManeuverModifier is int
? currentManeuverModifier as int
: int.tryParse(currentManeuverModifier.toString()) ?? 0,
distanceToStep: 0.0,
);
update();
} else {
_finishNavigation();
@@ -1241,6 +1259,7 @@ class NavigationController extends GetxController
if (!isMuted) {
Get.find<TextToSpeechController>().speakText(currentInstruction);
}
CarPlatformBridge.stopNavigation();
_flushBufferToServer();
update();
}
@@ -1303,6 +1322,34 @@ class NavigationController extends GetxController
return R * 2 * atan2(sqrt(a), sqrt(1 - a));
}
void _pushCarBridgeUpdate() {
if (!isNavigating) return;
final etaSeconds = _routeTotalDurationS > 0
? (_fullRouteCoordinates.isNotEmpty
? _routeTotalDurationS *
(_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) /
_fullRouteCoordinates.length
: 0.0)
: 0.0;
CarPlatformBridge.updateNavState(
lat: myLocation?.latitude ?? 0.0,
lng: myLocation?.longitude ?? 0.0,
bearing: _smoothedHeading,
speed: currentSpeed,
instruction: currentInstruction,
distanceToStep: _distanceToNextStepMeters,
totalDistance: _routeTotalDistanceM * (_fullRouteCoordinates.isNotEmpty
? (_fullRouteCoordinates.length - _lastTraveledIndexInFullRoute) /
_fullRouteCoordinates.length
: 0.0),
eta: etaSeconds,
maneuver: currentManeuverModifier is int
? currentManeuverModifier as int
: int.tryParse(currentManeuverModifier.toString()) ?? 0,
isNavigating: true,
);
}
double _kmToLatDelta(double km) => km / 111.32;
double _kmToLngDelta(double km, double lat) =>
km / (111.32 * cos(lat * pi / 180));
+3
View File
@@ -35,6 +35,7 @@ import 'splash_screen_page.dart';
import 'views/home/Captin/orderCaptin/order_request_page.dart';
import 'views/home/Captin/driver_map_page.dart';
import 'controller/profile/setting_controller.dart';
import 'controller/car_platform_bridge.dart';
import 'controller/voice_call_controller.dart';
import 'controller/functions/tts.dart';
@@ -265,6 +266,8 @@ void main() {
await createAllNotificationChannels();
await BackgroundServiceHelper.initialize();
CarPlatformBridge.ensureInitialized();
FirebaseMessaging.onBackgroundMessage(backgroundMessageHandler);
runApp(const MyApp());
}, (error, stack) {