Merging cursors during onLoadFinished() causes StaleDataException after rotation

1.3k views Asked by At

I'm loading some results from a database using a loaderManager. Unfortunately, the following code produces a StaleDataException after rotating the device:

@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor)
{
    // If we've returned results, then pass them and the webSearches cursor along to be displayed
    if(cursor.moveToFirst())
    {
        // Get a cursor containing additional web searches and merge it at the end of the results cursor 
        MatrixCursor searchesCursor = getWebSearchesCursor(searchTerm, false);
        Cursor[] cursors = { cursor, searchesCursor };
        // TODO: Figure out why merging cursors here causes staledataexception after rotation
        Cursor results = new MergeCursor(cursors);
        // Display the cursor in the ListView
        adapter.changeCursor(results);
    }
    // If no results were returned, then return suggestions for web searches
    else
    {
        // Get a cursor containing additional web searches 
        MatrixCursor noResults = getWebSearchesCursor(searchTerm, true);
        adapter.changeCursor(noResults);    
    }

    // Show the listView and hide the progress spinner
    toggleListView(SHOW);
}

The call to getWebSearchesCursor() returns a MatrixCursor with some additional search prompts to accompany any returned results. I discovered that changing adapter.changeCursor(results) to adapter.changeCursor(cursor) fixes the error, so it looks like merging a MatrixCursor to the returned cursor produces the error.

My question is, why?

If any results are returned, I'd like to be able to add additional items to the returned cursor so the user has the option to perform their search on a couple of websites. Is there a better way to merge cursors so that I don't get this exception after rotation?

2

There are 2 answers

0
IAmKale On BEST ANSWER

This issue came up again a couple of days ago and I was fortunate enough to stumble upon a solution.

I found out that I should have been using swapCursor() instead of changeCursor(). According to the Android docs:

Swap in a new Cursor, returning the old Cursor. Unlike changeCursor(Cursor), the returned old Cursor is not closed.

...

If the given new Cursor is the same instance is the previously set Cursor, null is also returned.

That last part seemed to be the key. The error mentioned in the question above could be traced back to the CursorAdapter choking on the merged cursor because it was closed when it tried to redraw the fragment after a rotation. By using swapCursor() instead, the CursorAdapter was able to reuse the "old" merged cursor instead of questioning its validity and throwing a StaleDataException.

I'm making some suppositions here; perhaps someone more knowledgeable in the inner-workings of Android could confirm or deny my reasoning.

0
Thorbear On

If you've started using swapCursor() instead of changeCursor() everywhere, then I hope you've also started handling cursor closing in those places.

changeCursor() will close the old cursor, this is intentional and works flawlessly when you're just directly using the cursor provided by onLoadFinished(). It is done this way so you don't have to worry about closing it.

When you rotate your device, the android system will check that the cursor it sent you last time has not yet been closed, and sends it again rather than spending resources on creating a new one. Your code wraps this cursor in a new instance of a MergeCursor which gets passed to changeCursor(), which sees that this is not the same object it got before, and decides to close the old instance. Since MergeCursor only wraps the cursors you pass it, rather than copying the data in them, your new instance now contains (at least) one closed cursor.

To correctly handle this you'll need to write some code of your own that checks if the cursor you get through onLoadFinished() is the same as one you have in your current MergeCursor instance, and only close the existing instance if you are getting a new cursor. Of course you'll also need to keep track of the other cursors wrapped in the same MergeCursor instance.