Fix NullPointerException when itinerary.legs is null in RealtimeService.startRealtimeUpdates()#1522
Fix NullPointerException when itinerary.legs is null in RealtimeService.startRealtimeUpdates()#1522varun-ai69 wants to merge 7 commits intoOneBusAway:mainfrom
Conversation
aaronbrethorst
left a comment
There was a problem hiding this comment.
Hey varun-ai69, thanks for taking the time to harden RealtimeService against null bundles and missing data. The core itinerary.legs null check is the right fix, and the explicit intent change for Android 14+ is a good modernization. The test for the null-legs path through the public API is well-structured. Before we can merge this, I will need you to make a couple changes:
Important
-
catch (Exception e)aroundnew ItineraryDescription(itinerary)is too broad (RealtimeService.java:375-379) — This will silently swallow programming errors likeIllegalArgumentExceptionfromTraverseMode.valueOf(),ClassCastException, or any future bugs in the constructor. The null-legs case is already handled by your guard at line 114, so by the timegetSimplifiedBundleis called,itinerary.legsshould not be null. Replace the try/catch with an explicit null/empty check:if (itinerary.legs == null || itinerary.legs.isEmpty()) { Log.w(TAG, "getSimplifiedBundle: itinerary has no legs"); return null; } ItineraryDescription desc = new ItineraryDescription(itinerary);
This handles the defensive case without swallowing unrelated exceptions.
-
Reflection-based tests are brittle (
RealtimeServiceTest.java:152-187) —testGetSimplifiedBundleWithMissingItineraryDoesNotCrashandtestGetSimplifiedBundleWithMissingNotificationTargetDoesNotCrashuse reflection to call the privategetSimplifiedBundle()method. If the method is renamed or its signature changes, these tests fail at runtime rather than compile time. The existingtestOnHandleIntentWithEmptyBundleDoesNotCrashalready covers the "missing itinerary" path through the public API. Please rewrite both tests to go throughonHandleIntentinstead. For the missing-NOTIFICATION_TARGET case, you'd need a bundle with realtime legs but no notification target:@Test public void testOnHandleIntentWithRealtimeLegsButNoNotificationTargetDoesNotCrash() { Bundle bundle = new Bundle(); ArrayList<Itinerary> itineraries = new ArrayList<>(); Itinerary it = new Itinerary(); Leg leg = new Leg(); leg.realTime = true; leg.mode = "BUS"; it.legs = new ArrayList<>(); it.legs.add(leg); itineraries.add(it); bundle.putSerializable(OTPConstants.ITINERARIES, itineraries); bundle.putInt(OTPConstants.SELECTED_ITINERARY, 0); // No NOTIFICATION_TARGET — exercises the null alarmIntent guard Intent intent = new Intent(OTPConstants.INTENT_START_CHECKS); intent.putExtras(bundle); try { mService.onHandleIntent(intent); } catch (NullPointerException e) { fail("Should not crash with realtime legs but missing NOTIFICATION_TARGET: " + e.getMessage()); } }
This also gives you coverage of the
alarmIntent == nullguard instartRealtimeUpdates(lines 125-129), which currently has no test.
Fit and Finish
-
Add a null guard in
disableListenForTripUpdates()(RealtimeService.java:307-310) —getAlarmIntent(null)currently can't return null (the null-return path only triggers whenbundle != null), butgetAlarmIntentnow has a null-returning contract anddisableListenForTripUpdatesdoesn't check for it. If someone later refactorsgetAlarmIntent,AlarmManager.cancel(null)will throw an NPE — and this method is called from error-recovery paths throughout the class:public void disableListenForTripUpdates() { Log.d(TAG, "Disable trip updates."); PendingIntent alarmIntent = getAlarmIntent(null); if (alarmIntent != null) { getAlarmManager().cancel(alarmIntent); } }
-
PR title has duplicate text — The title reads "Fix NullPointerException when itinerary.legs is null in RealtimeService.startRealtimeUpdates()Fix null itinerary legs". Please clean that up.
Thanks again, and I look forward to merging this change.
4f6097a to
e432996
Compare
|
@aaronbrethorst I’ve addressed the requested changes:
Could you please take another look when you have a moment? Thanks! |
aaronbrethorst
left a comment
There was a problem hiding this comment.
Varun, nice progress on this. All four items from round 1 have been addressed: the ItineraryDescription catch is replaced with an explicit itinerary.legs null/empty guard, the reflection-based tests are rewritten to use onHandleIntent(), the disableListenForTripUpdates() null guard is in place, and the PR title is cleaned up. The tests are well-structured — the null-legs test and the missing-NOTIFICATION_TARGET test both exercise meaningful code paths through the public API.
Two items remain before this can merge.
Critical Issues (0 found)
None.
Important Issues (2 found)
1. catch (Exception e) around copyIntoBundleSimple() is too broad
RealtimeService.java:387 — The ItineraryDescription try-catch was correctly replaced with an explicit guard (nice work), but copyIntoBundleSimple() still catches bare Exception. Looking at copyIntoBundleSimple(), the realistic failure is NullPointerException when getFrom() or getTo() returns null. A broad catch (Exception) will silently swallow unrelated errors like ClassCastException from corrupt bundle data or IllegalStateException from framework issues, making them invisible in production.
Narrow it to match the specific failure you're defending against:
try {
new TripRequestBuilder(params).copyIntoBundleSimple(extras);
} catch (NullPointerException e) {
Log.e(TAG, "getSimplifiedBundle: error copying trip params into bundle", e);
return null;
}2. Log.w at lines 127 and 326 should be Log.e
Both of these log a condition where realtime monitoring completely fails to start — that's an error, not a warning. The failure paths inside getSimplifiedBundle() already correctly use Log.e; the downstream callers should be consistent:
// Line 127
Log.e(TAG, "Not scheduling realtime updates - unable to build alarm PendingIntent");
// Line 326
Log.e(TAG, "getAlarmIntent: simplified bundle is null, returning null PendingIntent");Suggestions (0 found)
None.
Strengths
- The explicit
itinerary.legs == null || itinerary.legs.isEmpty()guard ingetSimplifiedBundle()(line 377) is a much better approach than catching exceptions from theItineraryDescriptionconstructor — it makes the guarded condition self-documenting - The
testOnHandleIntentWithRealtimeLegsButNoNotificationTargetDoesNotCrashtest carefully builds a valid transit leg withtripIdandendTimeso it passes throughItineraryDescriptionand reaches the NOTIFICATION_TARGET check - The
testOnHandleIntentWithNullItineraryLegsDoesNotCrashtest includes a helpful comment explaining whyDATE_TIMEis omitted from the bundle - The null guard in
disableListenForTripUpdates()is forward-looking good practice
Recommended Action
Request changes. Fix the broad catch (Exception) on copyIntoBundleSimple() (item #1) and elevate Log.w to Log.e at the two monitoring-failure paths (item #2). Both are quick fixes.
… getSimplifiedBundle
…s to use public API, add null guard in disableListenForTripUpdates
…onitoring failures
6115de3 to
078785f
Compare
|
@aaronbrethorst Thanks for the review! I have addressed all the requested changes: -Narrowed catch (Exception) to catch (NullPointerException) in copyIntoBundleSimple() Ready for re-review! |
…n RealtimeService
Summary
This PR fixes a potential NullPointerException in
RealtimeService.startRealtimeUpdates()whenitinerary.legsis null.Previously the method iterated directly over
itinerary.legswithout validating the field. If an itinerary existed but itslegslist wasnull, the service would crash when attempting to iterate over it.The fix adds a null check before iterating over the legs to ensure the service safely handles incomplete itinerary objects.
In addition, the tests were updated to exercise the service through the public API (
onHandleIntent()) instead of relying on reflection, and a defensive null guard was added indisableListenForTripUpdates()when cancelling alarms.Problem
In
RealtimeService.startRealtimeUpdates()the code previously did:If
itinerary.legs == null, this results in:NullPointerExceptionThis could occur if:
-the itinerary object is partially deserialized
-a malformed bundle is received
-external API returns an itinerary without legs
As a result, the service may crash while handling
INTENT_START_CHECKS.How to Reproduce
A test case demonstrates the issue.
Before the Fix
Running this test results in:
After the Fix
The service safely handles the case and no exception is thrown, allowing the test to pass.
Fix
A defensive null check was added before iterating over
itinerary.legsinRealtimeService.startRealtimeUpdates().Previously the code directly iterated over
itinerary.legs, which could cause aNullPointerExceptionif the itinerary object existed but itslegsfield wasnull.Updated implementation:
This ensures the service behaves safely even if the itinerary does not contain legs.
Additional Improvements
Added a defensive null check in
disableListenForTripUpdates()before cancelling alarms to avoid potentialNullPointerExceptionif aPendingIntentcannot be created.Tests
Added a regression test:
testOnHandleIntentWithNullItineraryLegsDoesNotCrashThis test verifies that
onHandleIntent()does not throw a NullPointerException whenitinerary.legsisnull.Impact
This change:
RealtimeServiceNo functional behavior changes occur for valid itineraries.