androidandroid-fragmentsandroid-cursoradapterandroid-cursorstaledataexception

Closing Cursor causes StaleDataException on backgrounding fragment with CursorAdapter


This is a follow up to my question here: How to close a cursor used in a for loop

The responses solved the "Cursor finalized without prior close" warning but it has caused a StaleDataException in a very particular situation.

If the list has been scrolled, this cursor closed...

Cursor cursor = null;
cursor = (Cursor) getListView().getItemAtPosition(n);
//do something
if(cursor != null) {
    cursor.close();
}

and the fragment backgrounded I get the following error:

09-15 21:16:58.240: E/test(21621): Exception
 FATAL EXCEPTION: main
 android.database.StaleDataException: Attempted to access a cursor after it has been closed.
    at android.database.BulkCursorToCursorAdaptor.throwIfCursorIsClosed(BulkCursorToCursorAdaptor.java:64)
    at android.database.BulkCursorToCursorAdaptor.getCount(BulkCursorToCursorAdaptor.java:70)
    at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:196)
    at android.database.CursorWrapper.moveToPosition(CursorWrapper.java:162)
    at android.support.v4.widget.CursorAdapter.getItemId(CursorAdapter.java:225)
    at android.widget.AbsListView.onSaveInstanceState(AbsListView.java:1782)
    at android.view.View.dispatchSaveInstanceState(View.java:11950)
    at android.view.ViewGroup.dispatchFreezeSelfOnly(ViewGroup.java:2685)
    at android.widget.AdapterView.dispatchSaveInstanceState(AdapterView.java:782)
    at android.view.ViewGroup.dispatchSaveInstanceState(ViewGroup.java:2671)
    at android.view.View.saveHierarchyState(View.java:11933)
    at android.support.v4.app.FragmentManagerImpl.saveFragmentViewState(FragmentManager.java:1608)
    at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1004)
    at android.support.v4.app.FragmentManagerImpl.removeFragment(FragmentManager.java:1212)
    at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:639)
    at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1478)
    at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:446)
    at android.os.Handler.handleCallback(Handler.java:615)
    at android.os.Handler.dispatchMessage(Handler.java:92)
    at android.os.Looper.loop(Looper.java:137)
    at android.app.ActivityThread.main(ActivityThread.java:4793)

This question seems to relate to a similar issue: Merging cursors during onLoadFinished() causes StaleDataException after rotation but it relates to merging cursors and suggests using swapCursor, I'm not sure how I would apply that to this situation.

My thoughts are that getListView().getItemAtPosition(n) must be returning a reference to the cursor rather than a new cursor and so when the activity is backgrounded and tries to access the now closed cursor whilst saving the fragment state it crashes. As mentioned earlier, it only crashes if the list view has been scrolled, I'm not sure why that should affect it.

How do I correctly close the cursor without causing a crash?

EDIT code in response to a comment asking to see how the cursor was loaded:

String[] desired_columns = { 
                MediaStore.Audio.Media._ID,  //this column is needed, even though it won't be displayed so the cursor can populate the listview
                MediaStore.Audio.Media.ARTIST,
                MediaStore.Audio.Media.ALBUM,
                MediaStore.Audio.Media.TITLE,
                MediaStore.Audio.Media.DURATION

                };

        String selectionStatement = MediaStore.Audio.Media.IS_MUSIC + " != ? AND " + MediaStore.Audio.Media.DURATION + " > ?";

        String[] selectionArguments = new String[2];
        selectionArguments[0] = "0";
        selectionArguments[1] = "7000";

        Cursor myCursor = getActivity().getContentResolver().query( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, 
                                                                    desired_columns, 
                                                                    selectionStatement,     //selection criteria
                                                                    selectionArguments,     //selection arguments like with SQL PDO use ? in criteria and put user input here  to avoid sql injection
                                                                    MediaStore.Audio.AudioColumns.ARTIST + " ASC"); //sort order of results
        //moveToFirst() returns false if the cursor is empty
        if (myCursor != null && myCursor.moveToFirst()) {
            customCursorAdapter myCursorAdapter = new customCursorAdapter(getActivity(), myCursor, 0);

            setListAdapter(myCursorAdapter);

            getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
            getListView().setSelector(R.drawable.list_selection_colouring);
        }

Solution

  • I found a solution which avoids the deprecated manageQuery and also doesn't use CursorLoaders.

    The solution was to close the cursor within onDestroy(). I tried closing it within onPause but this didn't help. Looking at the fragment lifecycle http://developer.android.com/guide/components/fragments.html#Lifecycle onDestroy is almost the last method called so the fragments state will already have been saved, making it safe to close the cursor.

    Here is the code I used:

    @Override
    public void onDestroy() {
       super.onDestroy();
       if (cursor != null) {
          cursor.close();
       }
    }