import 'dart:async'; import 'dart:convert'; import 'package:flutter/services.dart'; /// Model for trip data passed to the overlay class TripData { final String tripId; final String passengerName; final String pickupAddress; final String dropoffAddress; final double distanceKm; final double estimatedFare; final int estimatedMinutes; final double pickupLat; final double pickupLng; final String? passengerAvatarUrl; TripData({ required this.tripId, required this.passengerName, required this.pickupAddress, required this.dropoffAddress, required this.distanceKm, required this.estimatedFare, required this.estimatedMinutes, required this.pickupLat, required this.pickupLng, this.passengerAvatarUrl, }); Map toMap() => { 'tripId': tripId, 'passengerName': passengerName, 'pickupAddress': pickupAddress, 'dropoffAddress': dropoffAddress, 'distanceKm': distanceKm, 'estimatedFare': estimatedFare, 'estimatedMinutes': estimatedMinutes, 'pickupLat': pickupLat, 'pickupLng': pickupLng, 'passengerAvatarUrl': passengerAvatarUrl ?? '', }; factory TripData.fromMap(Map map) => TripData( tripId: map['tripId'] ?? '', passengerName: map['passengerName'] ?? '', pickupAddress: map['pickupAddress'] ?? '', dropoffAddress: map['dropoffAddress'] ?? '', distanceKm: (map['distanceKm'] ?? 0.0).toDouble(), estimatedFare: (map['estimatedFare'] ?? 0.0).toDouble(), estimatedMinutes: map['estimatedMinutes'] ?? 0, pickupLat: (map['pickupLat'] ?? 0.0).toDouble(), pickupLng: (map['pickupLng'] ?? 0.0).toDouble(), passengerAvatarUrl: map['passengerAvatarUrl'], ); factory TripData.fromJson(String json) => TripData.fromMap(jsonDecode(json) as Map); String toJson() => jsonEncode(toMap()); } /// Result returned when the driver accepts a trip class TripAcceptedResult { final String tripId; final DateTime acceptedAt; TripAcceptedResult({required this.tripId, required this.acceptedAt}); } /// Main plugin class — single entry point for Flutter side class TripOverlayPlugin { static const MethodChannel _channel = MethodChannel('trip_overlay_plugin'); // Stream controller for trip accepted events coming FROM Android overlay static final StreamController _tripAcceptedController = StreamController.broadcast(); // Stream controller for trip rejected/expired events static final StreamController _tripRejectedController = StreamController.broadcast(); static bool _isInitialized = false; /// Stream that fires when the driver taps "Accept" in the overlay static Stream get onTripAccepted => _tripAcceptedController.stream; /// Stream that fires when the driver rejects or overlay times out static Stream get onTripRejected => _tripRejectedController.stream; /// Initialize the plugin — call this once in main() or initState() static Future initialize() async { if (_isInitialized) return; _channel.setMethodCallHandler(_handleMethodCall); _isInitialized = true; } /// Handle incoming calls FROM Android → Flutter static Future _handleMethodCall(MethodCall call) async { switch (call.method) { case 'onTripAccepted': final tripId = call.arguments['tripId'] as String; _tripAcceptedController.add( TripAcceptedResult(tripId: tripId, acceptedAt: DateTime.now()), ); break; case 'onTripRejected': final tripId = call.arguments['tripId'] as String; _tripRejectedController.add(tripId); break; default: throw PlatformException( code: 'UNKNOWN_METHOD', message: 'Method ${call.method} not implemented', ); } } /// Check if SYSTEM_ALERT_WINDOW permission is granted static Future isPermissionGranted() async { final result = await _channel.invokeMethod('isPermissionGranted'); return result ?? false; } /// Open system settings to grant SYSTEM_ALERT_WINDOW permission static Future requestPermission() async { await _channel.invokeMethod('requestPermission'); } /// Show the trip overlay with the given [tripData] /// [autoCloseSeconds] — how long before auto-dismiss (default 30s) static Future showOverlay( TripData tripData, { int autoCloseSeconds = 30, }) async { final granted = await isPermissionGranted(); if (!granted) { await requestPermission(); return false; } final result = await _channel.invokeMethod('showOverlay', { 'tripData': tripData.toJson(), 'autoCloseSeconds': autoCloseSeconds, }); return result ?? false; } /// Programmatically close the overlay (e.g. if trip was cancelled) static Future hideOverlay() async { await _channel.invokeMethod('hideOverlay'); } /// Check if the overlay is currently visible static Future isOverlayActive() async { final result = await _channel.invokeMethod('isOverlayActive'); return result ?? false; } /// Dispose streams — call in app's dispose() static void dispose() { _tripAcceptedController.close(); _tripRejectedController.close(); } }