Fix crash on Android 16 security patch September 5 with home widget enabled #1779
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Closes SIMPL-16
Closes #1776
Fix
This was somewhat a ticking bomb - there was a latent race condition that somehow never appeared (or we never heard of) and now was super reliably reproducible with the latest security patch.
The crash we had was this:
It was weird because this part of the code hasn't been touched in years, however the mCursor which is instantiated in
onDataSetChanged()was null when the system tried to query the element count accessinggetCount()even when we expected aonDataSetChanged()call to be made first.Searching a bit I found a stackoverflow answer that copies the lifecycle diagram that used to be reliable (the linked official google documentation no longer has that diagram, implying one shouldn't make assumptions about the order of events being triggered).
Knowing this, used he help from Gemini to identify if there was a change on the system that would affect the
RemoteViewsFactorylifecycle steps:Whole answer below. 👇 👇 👇
The behavior you're observing—
getCount()being called beforeonDataSetChanged()has finished—is a classic and critical race condition in Android's widget development. Understanding theRemoteViewsFactorylifecycle is key to solving it.The Lifecycle of
RemoteViewsFactoryA
RemoteViewsFactorydoesn't have a standalone lifecycle like anActivity. Instead, its methods are callbacks invoked by the system when interacting with a collection widget (like aListViewon your home screen). The methods are called in a specific, important order.onCreate()onDataSetChanged()notifyAppWidgetViewDataChanged()).mCursoris correctly initialized.getCount()getViewAt().mCursor.getCount()).getViewAt(int position)RemoteViewsobject.RemoteViewsobject for the specificpositionusing the data you fetched inonDataSetChanged().getLoadingView()onDataSetChanged()is running).RemoteViewsobject to act as a loading indicator. Returningnullresults in a default system loading view.getViewTypeCount()1is very common.getItemId(int position)hasStableIds()getItemId()are consistent across data changes.trueif your IDs are stable.onDestroy()The Race Condition: Why
getCount()is Called BeforeonDataSetChanged()FinishesThis is the core of the problem and a well-known behavior of the API.
The system's UI thread cannot block and wait for your potentially long-running data query in
onDataSetChanged()to complete. It needs to know the list size immediately to start drawing the widget.This leads to the following sequence:
NoteListWidgetFactory.onCreate()is called. At this point, yourmCursorisnull.getCount()on the main thread to determine the list size.onDataSetChanged()on a background thread to start fetching data.getCount()is executed beforeonDataSetChanged()finishes initializingmCursor, your code will try to callmCursor.getCount(), resulting in aNullPointerException.The mention of a specific Android version or security patch is relevant because changes in the OS's internal process scheduling can make this race condition more or less likely to occur, but the fundamental issue exists in the API design itself across many Android versions.
The Solution: Defensive Coding in
getCount()To build a robust widget, you must anticipate this race condition and guard against it. The code you have provided already implements the correct solution.
Your
getCount()Method is Correct:Test
Review
Release