4

I have a series of ListView objects in Fragments that are being populated by a CursorAdapter which gets a Cursor from the LoaderManager for the activity. As I understand it, all database and Cursor close actions are completely handled by the LoaderManager and the ContentProvider, so at no point in any of the code am I calling .close() on anything.

Sometimes, however, I get this exception:

02-19 11:07:12.308 E/AndroidRuntime(18777): java.lang.IllegalStateException: attempt to re-open an already-closed object: android.database.sqlite.SQLiteQuery (mSql = SELECT * FROM privileges WHERE uuid!=?) 
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:33)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteQuery.fillWindow(SQLiteQuery.java:82)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:164)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.sqlite.SQLiteCursor.onMove(SQLiteCursor.java:147)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:178)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.database.CursorWrapper.moveToPosition(CursorWrapper.java:162)
02-19 11:07:12.308 E/AndroidRuntime(18777): at android.widget.CursorAdapter.getView(CursorAdapter.java:241)

I put some log code into my CursorAdapter that tells me when getView(...), getItem(...) or getItemId(...) are being called and it seems as though this is happening on the first getView(...) for a given adapter after a lot of getView(...)s for another adapter. It also happens after a user has navigated around the app a lot.

This makes me wonder if the Cursor for an adapter is being retained in the CursorAdapter, but being closed in error by the ContentProvider or the Loader. Is this possible? Should I be doing any housekeeping on the CursorAdapter based on app/activity/fragment lifecycle events?

ContentProvider query method:

class MyContentProvider extends ContentProvider {
//...

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
            String[] selectionArgs, String sortOrder) {
        SQLiteDatabase db = mOpenHelper.getReadableDatabase();
        Cursor query = db.query(getTableName(uri), projection, selection, selectionArgs, null, null, sortOrder);
        query.setNotificationUri(getContext().getContentResolver(), uri);
        return query;
    }

//...
}

Typical LoaderCallbacks:

LoaderCallbacks<Cursor> mCallbacks = new LoaderCallbacks<Cursor>() {

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        mArticleAdapter.swapCursor(null);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
        if(cursor.isClosed()) {
            Log.d(TAG, "CURSOR RETURNED CLOSED");
            Activity activity = getActivity();
            if(activity!=null) {
                activity.getLoaderManager().restartLoader(mFragmentId, null, mCallbacks);
            }
            return;
        }
        mArticleAdapter.swapCursor(cursor);
    }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        triggerArticleFeed();
        CursorLoader cursorLoader = null;

        if(id == mFragmentId) {
            cursorLoader = new CursorLoader(getActivity(),
                                            MyContentProvider.ARTICLES_URI,
                                            null,
                                            ArticlesContentHelper.ARTICLES_WHERE,
                                            ArticlesContentHelper.ARTICLES_WHEREARGS,
                                            null);
        }
        return(cursorLoader);
    }
};

CursorAdapter constructor:

public ArticlesCursorAdapter(Context context, Cursor c) {
    super(context, c, 0);
    mImageloader = new ImageLoader(context);
}

I have read this question but unfortunately it hasn't got the answer to my problem as it simply suggests using a ContentProvider, which I am.

IllegalStateException: attempt to re-open an already-closed object. SimpleCursorAdapter problems

IMPORTANT NEW INFORMATION THAT HAS JUST COME TO LIGHT

I discovered some other code elsewhere in the project that was NOT using Loaders and NOT managing its Cursors properly. I've just switched this code over to use the same pattern as above; however, if this fixes things, it would suggest that an unmanaged Cursor in one part of a project can kill a properly managed one elsewhere.

Stick around.

OUTCOME OF NEW INFORMATION

That did not fix it.

NEW IDEA

@Override
onDestroyView() {
    getActivity().getLoaderManager().destroyLoader(mFragmentId);
    //any other destroy-time code
    super.onDestroyView()
}

ie possibly yes, I should be doing housekeeping on the CursorAdapter (or rather the CursorLoader in line with lifecycle events).

OUTCOME OF NEW IDEA

Nope.

PREVIOUS IDEA

Turned out to work once I added in a minor tweak! However it's so complex that I should probably rewrite the entire question.

Community
  • 1
  • 1
Andrew Wyld
  • 7,133
  • 7
  • 54
  • 96
  • show us some code ... `ContentProvider.query` and `OnLoadCompleteListener.onLoadComplete` – Selvin Feb 19 '13 at 11:46
  • ok more info ... in `onLoadComplete` you should simply use `Adapter.swapCursor(newCursor);` ... next ... do not store Cursor instance in Adapter by yourself, just use `getCursor()` inside Adapter ... so after when Cursor swapped you will get new instance of Cursor ... – Selvin Feb 19 '13 at 11:53
  • @Selvin that's what I am doing, and I'm only directly using the cursor passed into `bindView(...)`. The crash is actually coming when the cursor is moved into position by the `CursorAdapter` code (which I haven't overridden). – Andrew Wyld Feb 19 '13 at 11:59
  • Oh, and there's no `OnLoadCompleteListener`, I am implementing `LoaderCallbacks`. – Andrew Wyld Feb 19 '13 at 12:02
  • Added in `ContentProvider#query(...)` and a typical `LoaderCallbacks`. There are lots of those around but they all look pretty much the same. – Andrew Wyld Feb 19 '13 at 12:09
  • @dymmeh I just realized something: I am calling `notifyChange(...)` from the `IntentService`, not the `ContentProvider`. I'm also doing typically a `delete(...)` and then a `bulkInsert(...)` before notifying. Could that cause it? – Andrew Wyld Feb 19 '13 at 18:00
  • `notifyChange(...)` should be called from the content provider on an insert or update. – ebarrenechea Feb 19 '13 at 19:20
  • @ebarrenchea I've changed it so it does this. Same bug. What about on delete? – Andrew Wyld Feb 20 '13 at 09:56
  • "Turned out to work once I added in a minor tweak! However it's so complex that I should probably rewrite the entire question." -- what minor tweak...? Should probably write an answer with your fix and accept it. Others (like me) will stumble upon this and not know how you resolved the problem... – Joe Sep 20 '13 at 18:40
  • Fair point. I have now completely forgotten. However, it boiled down to this: if you're using `Cursor` with a `CursorAdapter` and also directly, you have to make sure you handle the `Cursor` the same as `CursorAdapter` does: using the old management functions causes horrible, horrible behaviour. Source is here: https://github.com/android/platform_frameworks_support/blob/master/v4/java/android/support/v4/widget/CursorAdapter.java Look at lines 453 to 481. – Andrew Wyld Sep 23 '13 at 09:54

2 Answers2

0

Have you updated your data set? It could be the case that the cursor has been re-loaded due notifying a change in the content resolver:

getContentResolver().notifyChange(URI, null);

If you have set a notification URI, this would trigger your current cursor to close and a new cursor to be returned by the cursor loader. You can then grab the new cursor if you have registered a onLoadCompleteListener:

mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() {
    @Override
    public void onLoadComplete(Loader<Cursor> loader, Cursor cursor) {
        // Set your listview's CursorAdapter
    }
});
Mohammad Dehghan
  • 17,853
  • 3
  • 55
  • 72
Ljdawson
  • 12,091
  • 11
  • 45
  • 60
  • I am using this, and I've set the `Cursor` up to receive notifications on that URI too. I thought `CursorAdapter` was supposed to handle this automatically though? I am already implementing `LoaderCallbacks` and calling `CursorAdapter#swapCursor(...)` in there. – Andrew Wyld Feb 19 '13 at 12:01
  • I think this might be relevant, actually: I am calling `notifyChange(...)`. However, would you normally expect the `Loader` to hit `onLoadFinished(...)` in the `LoaderCallbacks` and if not will it hit `onLoadComplete(...)` in an `OnLoadCompleteListener`? This seems, well, a little like duplication. Please let me know if I need to do that. – Andrew Wyld Feb 19 '13 at 17:10
  • I spawned a new question: http://stackoverflow.com/questions/14963524/is-it-necessary-to-implement-both-loadercallbacks-and-onloadcompletelistener-to – Andrew Wyld Feb 19 '13 at 17:25
  • 1
    Looks like registering both kinds of listener is a killer: `02-19 17:46:25.139: E/AndroidRuntime(24886): java.lang.IllegalStateException: There is already a listener registered` – Andrew Wyld Feb 19 '13 at 17:47
0

You can try to path null instead cursor into adapter constructor. Then owerride SwapCursor(Cursor c) in adapter, move initialization of cursor data there and call it in OnLoadFinished(Loader loader, Cursor data) method of your data loader.

enter code here
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
    // ... building your query here
       mSimpleCursorAdapter = new mSimpleCursorAdapter(getActivity().getApplicationContext(),
           layout, null, from, to, flags);
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
       contentAdapter.swapCursor(data);
    }