part of 'deliveries.dart'; // ------------------------------------------------------------------------- // SCREEN 2: RIDER NAVIGATION (Live tracking + auto-opens Google Maps once) // ------------------------------------------------------------------------- class _RiderNavigationScreen extends StatefulWidget { final Map delivery; final _MyDeliveriesState? parentState; const _RiderNavigationScreen({ // ignore: unused_element_parameter super.key, required this.delivery, this.parentState, }); @override State<_RiderNavigationScreen> createState() => _RiderNavigationScreenState(); } class _RiderNavigationScreenState extends State<_RiderNavigationScreen> with WidgetsBindingObserver { GoogleMapController? mapController; bool _mapReady = false; bool _isDisposed = false; Position? _currentPosition; StreamSubscription? _positionStream; final Set _markers = {}; late final LatLng dropLocation; bool _isInitializing = true; bool _hasOpenedGoogleMaps = false; Timer? _autoOpenTimer; Timer? _pipLoaderTimer; bool _showPipExpandLoader = false; bool _wasInPipBeforePause = false; // ETA timer for full navigation screen (top-right corner) final CountDownController _navEtaController = CountDownController(); int _navEtaRemainingSeconds = 0; final DeliveriesController deliveriesController = Get.put( DeliveriesController(), permanent: true, ); final Floating floating = Floating(); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _resolveDropFromDelivery(); _initializeMap(); _initEtaForNav(); WidgetsBinding.instance.addPostFrameCallback((_) { _attemptAutoOpenNavigation(); }); _autoOpenTimer = Timer( const Duration(seconds: 3), () => _attemptAutoOpenNavigation(fromTimer: true), ); } // Initialize remaining ETA for navigation screen using same persisted end time Future _initEtaForNav() async { try { final prefs = await SharedPreferences.getInstance(); final orderId = widget.delivery['orderid']?.toString() ?? widget.delivery['OrderId']?.toString() ?? ''; if (orderId.isEmpty) return; final endKey = 'eta_endtime_$orderId'; final endSeconds = prefs.getInt(endKey); final nowSeconds = DateTime.now().millisecondsSinceEpoch ~/ 1000; int remaining = 0; // ✅ FIX: Checking (endSeconds > nowSeconds) caused the timer to RESET if user was late. // We must trust existing endSeconds even if it's in the past. if (endSeconds != null && endSeconds > 0) { remaining = endSeconds - nowSeconds; debugPrint('[NAV] Loaded saved ETA end time: $endSeconds (Remaining: $remaining)'); } else { // Only start a NEW timer if one doesn't exist final rawEta = widget.delivery['eta']; int etaMinutes = 0; if (rawEta != null) { etaMinutes = int.tryParse(rawEta.toString()) ?? 0; } if (etaMinutes > 0) { remaining = etaMinutes * 60; final newEndSeconds = nowSeconds + remaining; await prefs.setInt(endKey, newEndSeconds); debugPrint('[NAV] Saved NEW ETA end time: $newEndSeconds (Remaining: $remaining)'); } } if (!mounted) return; setState(() { _navEtaRemainingSeconds = remaining.clamp(0, 24 * 60 * 60); }); } catch (_) { // Ignore errors – timer simply won't show } } @override void dispose() { _isDisposed = true; WidgetsBinding.instance.removeObserver(this); _autoOpenTimer?.cancel(); _pipLoaderTimer?.cancel(); _positionStream?.cancel(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState lifecycleState) { if (!mounted) return; if (lifecycleState == AppLifecycleState.paused || lifecycleState == AppLifecycleState.inactive) { final wasInPip = deliveriesController.isPipEnabled.value; _wasInPipBeforePause = wasInPip; if (wasInPip) { _pipLoaderTimer?.cancel(); if (mounted) { setState(() { _showPipExpandLoader = true; }); } } } else if (lifecycleState == AppLifecycleState.resumed) { // When app comes back to foreground, consider PiP session ended. deliveriesController.isPipEnabled.value = false; // ✅ FIX: Refresh the timer when coming back from PiP/Background _initEtaForNav(); if (_wasInPipBeforePause) { _wasInPipBeforePause = false; _pipLoaderTimer?.cancel(); _pipLoaderTimer = Timer(const Duration(milliseconds: 900), () { if (!mounted) return; setState(() { _showPipExpandLoader = false; }); }); } } } Future enablePip( BuildContext context, { bool autoEnable = false, }) async { if (!mounted) return; debugPrint('[NAV] Enabling PiP for navigation screen'); const rational = Rational.landscape(); final screenSize = MediaQuery.of(context).size * MediaQuery.of(context).devicePixelRatio; final height = (screenSize.height * 0.5).toInt(); final width = (screenSize.width * 0.9).toInt(); final arguments = autoEnable ? OnLeavePiP( aspectRatio: rational, sourceRectHint: math.Rectangle( 0, (screenSize.height ~/ 1) - (height ~/ 1), screenSize.width.toInt(), height, ), ) : ImmediatePiP( aspectRatio: rational, sourceRectHint: math.Rectangle( ((screenSize.width - width) ~/ 2).toInt(), ((screenSize.height - height) ~/ 2).toInt(), width, height, ), ); final status = await floating.enable(arguments); deliveriesController.isPipEnabled.value = true; debugPrint('PiP enabled? $status'); } double _parseD(dynamic v) { if (v == null) return 0.0; if (v is num) return v.toDouble(); return double.tryParse(v.toString()) ?? 0.0; } void _resolveDropFromDelivery() { final d = widget.delivery; final double dropLat = _parseD( d['droplat'] ?? d['DropLat'] ?? d['deliverylat'], ); final double dropLon = _parseD( d['droplon'] ?? d['DropLon'] ?? d['deliverylong'], ); dropLocation = (dropLat != 0 && dropLon != 0) ? LatLng(dropLat, dropLon) : const LatLng(11.018356, 77.012596); } Future _initializeMap() async { try { if (mounted) { setState(() { Geolocator.getLastKnownPosition().then((lastPos) { if (lastPos != null && mounted) { setState(() { _currentPosition = lastPos; }); _addMarkers(LatLng(lastPos.latitude, lastPos.longitude)); _startLiveTracking(); } }); if (_currentPosition == null) { _currentPosition = Position( latitude: dropLocation.latitude, longitude: dropLocation.longitude, timestamp: DateTime.now(), accuracy: 0, altitude: 0, heading: 0, speed: 0, speedAccuracy: 0, altitudeAccuracy: 0, headingAccuracy: 0, ); _addMarkers(dropLocation); } _isInitializing = false; }); } _getCurrentLocation().then((_) { _startLiveTracking(); }); } catch (e) { debugPrint('[NAVIGATION] Error initializing map: $e'); if (mounted) { setState(() { if (_currentPosition == null) { _currentPosition = Position( latitude: dropLocation.latitude, longitude: dropLocation.longitude, timestamp: DateTime.now(), accuracy: 0, altitude: 0, heading: 0, speed: 0, speedAccuracy: 0, altitudeAccuracy: 0, headingAccuracy: 0, ); _addMarkers(dropLocation); } _isInitializing = false; }); } } } Future _getCurrentLocation() async { bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null && mounted) { setState(() { _currentPosition = lastPos; _isInitializing = false; }); } Geolocator.openLocationSettings(); return; } LocationPermission permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); } if (permission == LocationPermission.deniedForever || permission == LocationPermission.denied) { final lastPos = await Geolocator.getLastKnownPosition(); if (lastPos != null && mounted) { setState(() { _currentPosition = lastPos; _isInitializing = false; }); _addMarkers(LatLng(lastPos.latitude, lastPos.longitude)); } else if (mounted) { setState(() => _isInitializing = false); } return; } Position? pos; try { pos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.medium, timeLimit: const Duration(seconds: 5), ).timeout(const Duration(seconds: 5)); } catch (e) { debugPrint('[NAVIGATION] Timeout getting position, using last known: $e'); pos = await Geolocator.getLastKnownPosition(); } if (pos != null && mounted) { setState(() { _currentPosition = pos; _isInitializing = false; }); _addMarkers(LatLng(pos.latitude, pos.longitude)); } else if (mounted) { setState(() => _isInitializing = false); } } void _startLiveTracking() { _positionStream = Geolocator.getPositionStream( locationSettings: const LocationSettings( accuracy: LocationAccuracy.bestForNavigation, distanceFilter: 5, ), ).listen((Position position) { if (mounted) { setState(() { _currentPosition = position; }); _addMarkers(LatLng(position.latitude, position.longitude)); final distance = Geolocator.distanceBetween( position.latitude, position.longitude, dropLocation.latitude, dropLocation.longitude, ); deliveriesController.deliverableDistance.value = distance; _safeAnimateCamera( CameraUpdate.newCameraPosition( CameraPosition( target: LatLng(position.latitude, position.longitude), zoom: 16.5, tilt: 45, ), ), ); } }); } void _addMarkers(LatLng riderPos) { _markers ..clear() ..add( Marker( markerId: const MarkerId('rider'), position: riderPos, icon: BitmapDescriptor.defaultMarkerWithHue( BitmapDescriptor.hueAzure, ), infoWindow: const InfoWindow(title: 'You (Rider)'), ), ) ..add( Marker( markerId: const MarkerId('drop'), position: dropLocation, icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueRed), infoWindow: const InfoWindow(title: 'Drop Location'), ), ); } Future _attemptAutoOpenNavigation({bool fromTimer = false}) async { if (!mounted || _hasOpenedGoogleMaps) return; final bool opened = await _openGoogleMapsNavigation(); if (!opened && fromTimer) { debugPrint('[NAVIGATION] Auto open failed, waiting for manual trigger'); } } Future _openGoogleMapsNavigation() async { if (!mounted) return false; if (_hasOpenedGoogleMaps) return true; try { if (Platform.isAndroid) { try { // ✅ CRITICAL: ENFORCE PiP when starting navigation (compulsory for active deliveries) final dc = Get.put(DeliveriesController(), permanent: true); // Enable PiP using floating package (primary) if (!dc.isPipEnabled.value) { await enablePip(context); // Wait a moment for PiP to activate await Future.delayed(const Duration(milliseconds: 300)); // Also tell native side (if available) – best-effort const channel = MethodChannel('nearle/pip'); try { await channel.invokeMethod('enterPip'); } catch (_) {} dc.isPipEnabled.value = true; } } on PlatformException catch (e) { debugPrint('[NAV] Method channel PiP failed: ${e.message}'); } catch (e) { debugPrint('[NAV] Error enabling PiP: $e'); } } Position? currentPos; try { currentPos = await Geolocator.getLastKnownPosition(); } catch (_) {} if (currentPos == null) { try { currentPos = await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.medium, timeLimit: const Duration(seconds: 2), ); } catch (_) {} } final originLat = currentPos?.latitude ?? dropLocation.latitude; final originLng = currentPos?.longitude ?? dropLocation.longitude; final destLat = dropLocation.latitude; final destLng = dropLocation.longitude; final Uri nativeUri = Uri.parse( 'google.navigation:q=$destLat,$destLng&mode=d', ); final Uri webUri = Uri.parse( 'https://www.google.com/maps/dir/?api=1' '&origin=$originLat,$originLng' '&destination=$destLat,$destLng' '&travelmode=driving' '&dir_action=navigate', ); // Try native intent first without relying on canLaunch (can be flaky) bool launched = false; try { launched = await launchUrl( nativeUri, mode: LaunchMode.externalApplication, ); } catch (e) { debugPrint('[NAVIGATION] Native intent failed: $e'); } if (!launched) { try { launched = await launchUrl( webUri, mode: LaunchMode.externalApplication, ); } catch (e) { debugPrint('[NAVIGATION] Web intent failed: $e'); } } if (launched && mounted) { setState(() { _hasOpenedGoogleMaps = true; }); debugPrint('[NAVIGATION] Opened Google Maps'); return true; } debugPrint('[NAVIGATION] Could not open Google Maps'); } catch (e) { debugPrint('[NAVIGATION] Error opening Google Maps: $e'); } return false; } @override Widget build(BuildContext context) { return Obx(() { if (deliveriesController.isPipEnabled.value) { final rawEta = widget.delivery['eta']; int etaMinutes = 0; if (rawEta != null) { etaMinutes = int.tryParse(rawEta.toString()) ?? 0; } return PipInfoCard( orderId: widget.delivery['orderid']?.toString() ?? widget.delivery['OrderId']?.toString() ?? 'N/A', etaMinutes: etaMinutes, ); } return Scaffold( body: Stack( children: [ GoogleMap( mapType: MapType.normal, onMapCreated: (controller) { mapController = controller; _mapReady = true; }, initialCameraPosition: CameraPosition( target: LatLng( _currentPosition?.latitude ?? dropLocation.latitude, _currentPosition?.longitude ?? dropLocation.longitude, ), zoom: 15, tilt: 45, ), markers: _markers, myLocationEnabled: true, myLocationButtonEnabled: false, trafficEnabled: true, compassEnabled: true, buildingsEnabled: true, tiltGesturesEnabled: true, ), if (!_hasOpenedGoogleMaps) Positioned( top: 50, left: 0, right: 0, child: Center( child: Container( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 12, ), decoration: BoxDecoration( color: ColorConstants.primaryColor, borderRadius: BorderRadius.circular(25), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ), const SizedBox(width: 12), Text( 'Opening Google Maps...', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), ], ), ), ), ), Positioned( top: _hasOpenedGoogleMaps ? 50 : 120, left: 16, child: CircleAvatar( backgroundColor: Colors.white, child: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () { _autoOpenTimer?.cancel(); // Navigate back to BottomPage on Deliveries tab Get.offAll(() => const BottomPage(initialIndex: 1)); }, ), ), ), if (_hasOpenedGoogleMaps) Positioned( top: 50, right: 16, child: CircleAvatar( backgroundColor: Colors.white, child: IconButton( icon: const Icon(Icons.navigation, color: Colors.blue), onPressed: () async { await _openGoogleMapsNavigation(); }, tooltip: 'Open Google Maps', ), ), ), // Small real-time ETA timer on top-right in full navigation (not in PiP) if (!deliveriesController.isPipEnabled.value && _navEtaRemainingSeconds > 0) Positioned( top: 110, right: 16, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.15), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: 32, height: 32, child: CircularCountDownTimer( key: ValueKey('nav_timer_$_navEtaRemainingSeconds'), // Force rebuild if duration changes significantly duration: _navEtaRemainingSeconds, initialDuration: 0, controller: _navEtaController, width: 32, height: 32, ringColor: ColorConstants.primaryColor .withValues(alpha: 0.15), fillColor: ColorConstants.primaryColor, backgroundColor: Colors.white, strokeWidth: 3, strokeCap: StrokeCap.round, textStyle: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: ColorConstants.primaryColor, fontFamily: FontConstants.fontFamily, ), isReverse: true, isReverseAnimation: true, isTimerTextShown: true, autoStart: true, timeFormatterFunction: (defaultFormatter, duration) { return duration.inSeconds <= 0 ? 'Out' : defaultFormatter(duration); }, ), ), const SizedBox(width: 6), Text( 'ETA', style: TextStyle( fontSize: 11, fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily, color: Colors.black87, ), ), ], ), ), ), Align( alignment: Alignment.bottomCenter, child: Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(20)), boxShadow: [ BoxShadow( color: Colors.black26, blurRadius: 8, offset: Offset(0, -2), ), ], ), child: SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Customer Details', style: TextStyle( fontSize: 22, fontWeight: FontWeight.bold, fontFamily: FontConstants.fontFamily, ), ), InkWell( onTap: () async { // Enable PiP mode first before launching dialer if (!deliveriesController.isPipEnabled.value) { await enablePip(context); // Wait a moment for PiP to activate await Future.delayed( const Duration(milliseconds: 300), ); } final phone = (widget.delivery['deliverycontactno'] ?? '') .toString(); final bool success = await launchPhoneDialer( phone.isNotEmpty ? phone : '9876543210', ); if (!success && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Could not launch dialer'), ), ); } }, child: Image.asset( 'assets/images/phone-call .png', height: 27, width: 27, errorBuilder: (c, e, s) => const Icon( Icons.phone, size: 27, color: Colors.green, ), ), ), ], ), const SizedBox(height: 10), const Divider(thickness: 1.5), const SizedBox(height: 10), _buildCustomerInfo(), const SizedBox(height: 20), Row( children: [ Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, minimumSize: const Size(0, 48), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), onPressed: () async { // Optimize: Run PiP enable and Prefs saving in parallel final d = widget.delivery; final double? lat = double.tryParse("${d['droplat']}"); final double? lng = double.tryParse("${d['droplon']}"); if (lat == null || lng == null) { debugPrint("[RIDE] Invalid drop location"); return; } final prefsFuture = SharedPreferences.getInstance().then((prefs) async { await prefs.setString("drop_lat", lat.toString()); await prefs.setString("drop_lng", lng.toString()); }); // PiP Logic if (!deliveriesController.isPipEnabled.value) { try { debugPrint('[NAV] Enforcing PiP before Ride button'); // Don't await extremely long, just enough to start transition await enablePip(context); await Future.delayed(const Duration(milliseconds: 150)); } catch (e) { debugPrint('[NAV] Error enabling PiP: $e'); } } // Construct URL (Map uses current location by default if origin not specified) final String url = "https://www.google.com/maps/dir/?api=1&destination=$lat,$lng&travelmode=driving"; final Uri uri = Uri.parse(url); // Fire and forget prefs prefsFuture.ignore(); try { // Direct launch is faster and avoids package visibility queries await launchUrl(uri, mode: LaunchMode.externalApplication); debugPrint("[RIDE] Opening Google Maps..."); } catch (e) { debugPrint("[RIDE] Could not launch Maps: $e"); } }, child: Text( 'Ride', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), SizedBox(width: 12), // space between buttons Expanded( child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: ColorConstants.primaryColor, minimumSize: const Size( 0, 48, ), // FIX: no infinity width shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), ), onPressed: () async { final result = await showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical( top: Radius.circular(20), ), ), builder: (context) => _DeliveryBottomSheet( delivery: widget.delivery, parentState: widget.parentState, ), ); if (result == true) { // ✅ If delivery updated/skipped, close navigation screen immediately if (mounted && Navigator.canPop(context)) { Navigator.pop(context); } } }, child: Text( 'Update', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ], ), ], ), ), ), ), if (_showPipExpandLoader) Positioned.fill( child: Container( color: Colors.black.withOpacity(0.45), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox( width: 56, height: 56, child: CircularProgressIndicator( strokeWidth: 4, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ), const SizedBox(height: 16), Text( 'Restoring full view...', style: TextStyle( color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily, ), ), ], ), ), ), ], ), ); }); } Widget _buildCustomerInfo() { final rawCollection = widget.delivery['collectionamt']; double collectionAmt = 0.0; if (rawCollection != null) { if (rawCollection is num) { collectionAmt = rawCollection.toDouble(); } else { collectionAmt = double.tryParse(rawCollection.toString()) ?? 0.0; } } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildInfoRow( 'Name:', (widget.delivery['deliverycustomer'] ?? 'Customer').toString(), ), const SizedBox(height: 10), _buildInfoRow( 'Address:', (widget.delivery['deliveryaddress'] ?? 'Address not available') .toString(), isExpanded: true, ), const SizedBox(height: 10), _buildInfoRow( 'Order ID:', '#${(widget.delivery['orderid'] ?? '').toString()}', ), if (collectionAmt > 0) ...[ const SizedBox(height: 10), _buildInfoRow( 'To Collect:', '₹${collectionAmt.toStringAsFixed(0)}', ), ], ], ); } Widget _buildInfoRow(String label, String value, {bool isExpanded = false}) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, fontFamily: FontConstants.fontFamily, ), ), const SizedBox(width: 15), isExpanded ? Expanded( child: Text( value, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 18, fontFamily: FontConstants.fontFamily, color: Colors.black87, ), ), ) : Text( value, style: TextStyle( fontSize: 18, fontFamily: FontConstants.fontFamily, color: Colors.black87, ), ), ], ); } void _safeAnimateCamera(CameraUpdate update) { if (!_mapReady || _isDisposed || mapController == null) return; try { mapController?.animateCamera(update); } catch (e) { debugPrint('[NAVIGATION] animateCamera skipped: $e'); } } }