| 1 | import 'package:home_widget/home_widget.dart'; |
| 2 | import 'package:intl/intl.dart'; |
| 3 | import 'package:openapi/openapi.dart'; |
| 4 | import 'package:aero/services/main.dart'; |
| 5 | import 'package:aero/extensions/datetime.dart'; |
| 6 | |
| 7 | class WidgetUpdateService { |
| 8 | static final FlightDataService |
| 9 | |
| 10 | /// Updates the home widget with updated flight status from backend |
| 11 | /// Only updates if widget already has flight data |
| 12 | static Future<void |
| 13 | try { |
| 14 | // Check if widget has flight data |
| 15 | final flightId = await HomeWidget |
| 16 | final flightIata = await HomeWidget |
| 17 | final flightIcao = await HomeWidget |
| 18 | final flightDateStr = |
| 19 | await HomeWidget.getWidgetData< |
| 20 | |
| 21 | // If no flight is setup in widget, do nothing |
| 22 | if (flightId == null || |
| 23 | flightIata == null || |
| 24 | flightIcao == null || |
| 25 | flightDateStr == null) { |
| 26 | print('No flight setup in widget, skipping update'); |
| 27 | return; |
| 28 | } |
| 29 | |
| 30 | // Parse the date |
| 31 | DateTime flightDate; |
| 32 | try { |
| 33 | flightDate = DateTime.parse(flightDateStr) |
| 34 | } catch (e) { |
| 35 | print('Invalid flight date in widget: $flightDateStr' |
| 36 | await _clearWidgetData(); |
| 37 | await HomeWidget.updateWidget( |
| 38 | androidName: 'CurrentActiveFlightReceiver', |
| 39 | qualifiedAndroidName: |
| 40 | "fyi.procrastinator.aero.widgets.current_active_flight.CurrentActiveFlightReceiver"); |
| 41 | return; |
| 42 | } |
| 43 | |
| 44 | // Check if flight has concluded (arrival time has passed) |
| 45 | final arrivalTimeStr = |
| 46 | await HomeWidget.getWidgetData< |
| 47 | if (arrivalTimeStr != null) { |
| 48 | try { |
| 49 | final arrivalTime = DateTime. |
| 50 | final now = DateTime. |
| 51 | |
| 52 | // If arrival time has passed by more than 1 hour, clear widget |
| 53 | if (now.difference(arrivalTime).inHours |
| 54 | print('Flight has concluded, clearing widget'); |
| 55 | await _clearWidgetData(); |
| 56 | await HomeWidget.updateWidget( |
| 57 | androidName: 'CurrentActiveFlightReceiver', |
| 58 | qualifiedAndroidName: |
| 59 | "fyi.procrastinator.aero.widgets.current_active_flight.CurrentActiveFlightReceiver", |
| 60 | ); |
| 61 | return; |
| 62 | } |
| 63 | } catch (e) { |
| 64 | print('Error parsing arrival time: $e' |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | // Fetch updated flight status with forceUpdate=true |
| 69 | final updatedFlight = await _flightService. |
| 70 | flightIata, |
| 71 | flightIcao, |
| 72 | date: flightDate, |
| 73 | forceUpdate: true, |
| 74 | faFlightId: flightId, |
| 75 | ); |
| 76 | |
| 77 | if (updatedFlight == null) { |
| 78 | print('Failed to fetch updated flight info'); |
| 79 | return; |
| 80 | } |
| 81 | |
| 82 | // Extract flight information and update widget |
| 83 | await _updateWidgetWithFlightData(updatedFlight); |
| 84 | |
| 85 | // Trigger widget update |
| 86 | await HomeWidget.updateWidget( |
| 87 | androidName: 'CurrentActiveFlightReceiver', |
| 88 | qualifiedAndroidName: |
| 89 | "fyi.procrastinator.aero.widgets.current_active_flight.CurrentActiveFlightReceiver"); |
| 90 | } catch (e) { |
| 91 | print('Error updating widget: $e' |
| 92 | // Don't clear on error, keep existing data |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | /// Updates the widget with specific flight data |
| 97 | static Future<void |
| 98 | FlightResponseEntity flight) async { |
| 99 | try { |
| 100 | // Extract required data from flight entity |
| 101 | final airline = flight.airline; |
| 102 | final airlineLogo = airline.image ?? |
| 103 | "https://www.flightaware.com/images/airline_logos/180px/${airline.icao} |
| 104 | final flightNumberIata = |
| 105 | flight.flightAwareData?.identIata ?? flight.flightNo |
| 106 | final flightNumberIcao = |
| 107 | flight.flightAwareData?.identIcao ?? flight.flightNo |
| 108 | |
| 109 | final departure = flight.departure; |
| 110 | final arrival = flight.arrival; |
| 111 | |
| 112 | final destinationName = arrival.airport. |
| 113 | final arrivalAirportCode = |
| 114 | arrival.airport.iata ?? arrival.airport. |
| 115 | final departureAirportCode = |
| 116 | departure.airport.iata ?? departure.airport. |
| 117 | |
| 118 | // Format times using airport timezones |
| 119 | final departureTimeLocal = _formatTime( |
| 120 | departure.predictedTime?.utc ?? |
| 121 | departure.revisedTime?.utc ?? |
| 122 | departure.scheduledTime?.utc, |
| 123 | timezone: departure.airport.timeZone, |
| 124 | ); |
| 125 | final arrivalTimeLocal = _formatTime( |
| 126 | arrival.predictedTime?.utc ?? |
| 127 | arrival.revisedTime?.utc ?? |
| 128 | arrival.scheduledTime?.utc, |
| 129 | timezone: arrival.airport.timeZone, |
| 130 | ); |
| 131 | |
| 132 | // Determine delay/expedite status |
| 133 | final isDelayInDeparture = _isDelayed( |
| 134 | departure.scheduledTime?.utc, |
| 135 | departure.predictedTime?.utc ?? departure.revisedTime |
| 136 | ); |
| 137 | final isDelayInArrival = _isDelayed( |
| 138 | arrival.scheduledTime?.utc, |
| 139 | arrival.predictedTime?.utc ?? arrival.revisedTime |
| 140 | ); |
| 141 | final isExpediteInDeparture = _isExpedited( |
| 142 | departure.scheduledTime?.utc, |
| 143 | departure.predictedTime?.utc ?? departure.revisedTime |
| 144 | ); |
| 145 | final isExpediteInArrival = _isExpedited( |
| 146 | arrival.scheduledTime?.utc, |
| 147 | arrival.predictedTime?.utc ?? arrival.revisedTime |
| 148 | ); |
| 149 | |
| 150 | // Save data to home_widget storage |
| 151 | await HomeWidget.saveWidgetData( |
| 152 | await HomeWidget.saveWidgetData( |
| 153 | await HomeWidget.saveWidgetData( |
| 154 | await HomeWidget.saveWidgetData( |
| 155 | await HomeWidget.saveWidgetData( |
| 156 | 'arrival_airport_code', arrivalAirportCode); |
| 157 | await HomeWidget.saveWidgetData( |
| 158 | 'departure_airport_code', departureAirportCode); |
| 159 | await HomeWidget.saveWidgetData( |
| 160 | 'departure_time_local', departureTimeLocal); |
| 161 | await HomeWidget.saveWidgetData( |
| 162 | await HomeWidget.saveWidgetData( |
| 163 | 'is_delay_in_departure', isDelayInDeparture); |
| 164 | await HomeWidget.saveWidgetData( |
| 165 | await HomeWidget.saveWidgetData( |
| 166 | 'is_expedite_in_departure', isExpediteInDeparture); |
| 167 | await HomeWidget.saveWidgetData( |
| 168 | 'is_expedite_in_arrival', isExpediteInArrival); |
| 169 | |
| 170 | // Store flight metadata for polling updates |
| 171 | await HomeWidget.saveWidgetData( |
| 172 | 'flight_id', flight.flightAwareData?.id ?? |
| 173 | await HomeWidget.saveWidgetData( |
| 174 | await HomeWidget.saveWidgetData( |
| 175 | await HomeWidget.saveWidgetData( |
| 176 | 'flight_date', flight.date.toIso8601String |
| 177 | |
| 178 | // Store scheduled arrival time for checking if flight has concluded |
| 179 | final arrivalScheduled = arrival.scheduledTime? |
| 180 | arrival.predictedTime?.utc ?? |
| 181 | arrival.revisedTime?.utc; |
| 182 | if (arrivalScheduled != null) { |
| 183 | await HomeWidget.saveWidgetData( |
| 184 | 'arrival_time_scheduled', |
| 185 | arrivalScheduled, |
| 186 | ); |
| 187 | } |
| 188 | } catch (e) { |
| 189 | print('Error updating widget with flight data: $e' |
| 190 | throw e; |
| 191 | } |
| 192 | } |
| 193 | |
| 194 | /// Clears all widget data |
| 195 | static Future<void |
| 196 | await HomeWidget.saveWidgetData( |
| 197 | await HomeWidget.saveWidgetData( |
| 198 | await HomeWidget.saveWidgetData( |
| 199 | await HomeWidget.saveWidgetData( |
| 200 | await HomeWidget.saveWidgetData( |
| 201 | await HomeWidget.saveWidgetData( |
| 202 | await HomeWidget.saveWidgetData( |
| 203 | await HomeWidget.saveWidgetData( |
| 204 | await HomeWidget.saveWidgetData( |
| 205 | await HomeWidget.saveWidgetData( |
| 206 | await HomeWidget.saveWidgetData( |
| 207 | await HomeWidget.saveWidgetData( |
| 208 | |
| 209 | // Clear flight metadata |
| 210 | await HomeWidget.saveWidgetData( |
| 211 | await HomeWidget.saveWidgetData( |
| 212 | await HomeWidget.saveWidgetData( |
| 213 | await HomeWidget.saveWidgetData( |
| 214 | await HomeWidget.saveWidgetData( |
| 215 | } |
| 216 | |
| 217 | /// Formats a time string to HH:mm format in the specified timezone |
| 218 | /// If timezone is not provided or invalid, falls back to UTC |
| 219 | static String _formatTime |
| 220 | if (timeString == null || timeString. |
| 221 | |
| 222 | try { |
| 223 | final dateTime = DateTime. |
| 224 | |
| 225 | // If timezone is provided and valid, convert to that timezone |
| 226 | if (timezone != null && timezone. |
| 227 | try { |
| 228 | return dateTime.toTimezoneString(timezone, |
| 229 | } catch (e) { |
| 230 | // If timezone conversion fails, fall back to UTC formatting |
| 231 | print('Error converting time to timezone $timezone: |
| 232 | return DateFormat('HH:mm') |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | // Fall back to UTC formatting if no timezone provided |
| 237 | return DateFormat('HH:mm') |
| 238 | } catch (e) { |
| 239 | return '--:--'; |
| 240 | } |
| 241 | } |
| 242 | |
| 243 | /// Checks if a flight is delayed (estimated > scheduled by more than 5 minutes) |
| 244 | static bool _isDelayed |
| 245 | if (scheduledTime == null || estimatedTime == |
| 246 | |
| 247 | try { |
| 248 | final scheduled = DateTime. |
| 249 | final estimated = DateTime. |
| 250 | final diff = estimated.difference |
| 251 | return diff.inMinutes > 5 |
| 252 | } catch (e) { |
| 253 | return false; |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | /// Checks if a flight is expedited (estimated < scheduled by more than 5 minutes) |
| 258 | static bool _isExpedited |
| 259 | if (scheduledTime == null || estimatedTime == |
| 260 | |
| 261 | try { |
| 262 | final scheduled = DateTime. |
| 263 | final estimated = DateTime. |
| 264 | final diff = scheduled.difference |
| 265 | return diff.inMinutes > 5 |
| 266 | } catch (e) { |
| 267 | return false; |
| 268 | } |
| 269 | } |
| 270 | |
| 271 | /// Updates widget immediately when a flight is fetched in the app |
| 272 | static Future<void |
| 273 | FlightResponseEntity flight) async { |
| 274 | try { |
| 275 | await _updateWidgetWithFlightData(flight); |
| 276 | await HomeWidget.updateWidget( |
| 277 | androidName: 'CurrentActiveFlightReceiver', |
| 278 | qualifiedAndroidName: |
| 279 | "fyi.procrastinator.aero.widgets.current_active_flight.CurrentActiveFlightReceiver"); |
| 280 | } catch (e) { |
| 281 | print('Error updating widget with flight: $e' |
| 282 | } |
| 283 | } |
| 284 | |
| 285 | /// Clears widget data (public method for external use) |
| 286 | static Future<void |
| 287 | try { |
| 288 | await _clearWidgetData(); |
| 289 | await HomeWidget.updateWidget( |
| 290 | androidName: 'CurrentActiveFlightReceiver', |
| 291 | qualifiedAndroidName: |
| 292 | "fyi.procrastinator.aero.widgets.current_active_flight.CurrentActiveFlightReceiver"); |
| 293 | } catch (e) { |
| 294 | print('Error clearing widget: $e' |
| 295 | } |
| 296 | } |
| 297 | } |