Update: 2026-07-04 16:16:49
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user