Using pagination with CursorLoader and MergeCursor closes old cursors - android

As the title says, when trying to paginate a ListView backed by a SimpleCursorAdapter and a CursorLoader, old cursors are getting closed, thus the below exception is being thrown. The first 2 pages load just fine (the first isn't using the MergeCursor and the second page is the first one to use the MergeCursor). I don't call any close() on any cursor whatsoever.
What is interesting is that while debugging, I cannot see the closed flags on any cursor being set as true, for what it's worth. Might be an issue with the MergeCursor then. Let me know if you guys have any solutions, I'm out of ideas.
Stack Trace:
android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method.
at android.database.AbstractWindowedCursor.checkPosition(AbstractWindowedCursor.java:139)
at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:74)
Code:
private List<Cursor> mCursorsList = new ArrayList<>();
#Override
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount)
{
if (!isLoading && !isAllLoaded &&
firstVisibleItem != 0 &&
firstVisibleItem == totalItemCount - visibleItemCount)
getActivity().getSupportLoaderManager().restartLoader(LOADER_ID, null, this);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, final Cursor data)
{
Cursor oldCursor = mCursorAdapter.getCursor();
mCursorsList.add(data);
if (oldCursor != null)
{
Cursor[] cursorArray = mCursorsList.toArray(new Cursor[mCursorsList.size()]);
MergeCursor newCursor = new MergeCursor(cursorArray);
mCursorAdapter.swapCursor(newCursor);
}
else // first cursor
{
mCursorAdapter.swapCursor(data);
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader)
{
}

The main cause of this issue is that the CursorLoader manages the inner cursor, so whenever it needs to open a new one (such as a new page query), it closes the old cursor.
For a more simple pagination implementation, don't query with offsets, just bump the limit on every page, so that the new cursor contains all previous pages as well. Also, as Ian Lake suggested on Google+, sometimes you don't even need pagination, especially if you are doing complicated joins or sorting the data.

Related

cursor.moveToNext() not working in onLoadFinished after screen rotation

I'm using custom RecyclerView.Adapter to show the list items. I'm using LoaderManager.LoaderCallbacks<Cursor> to fetch the data from the database and in onLoadFinished() I use custom method to extract the data from the cursor and store it in the custom ArrayList one by one using while(cursor.moveToNext()). The list is loaded successfully. But upon screen rotation the list goes off.
When I debugged the code I got to know that the onLoadFinished() is called on screen rotation but the while loop is not working even if the cursor is not null and cursor count is greater than 0.
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
loadAndroidFlavours(cursor);
mCursor = cursor;
invalidateOptionsMenu();
}
private void loadAndroidFlavours(Cursor cursor) {
if (cursor.getCount() != 0) {
while (cursor.moveToNext()) {
int columnIndexID = cursor.getColumnIndex(AndroidFlavourEntry._ID);
int columnIndexFlavour = cursor.getColumnIndex(AndroidFlavourEntry.COLUMN_FLAVOUR);
int columnIndexVersion = cursor.getColumnIndex(AndroidFlavourEntry.COLUMN_VERSION);
int ID = cursor.getInt(columnIndexID);
String flavour = cursor.getString(columnIndexFlavour);
String version = cursor.getString(columnIndexVersion);
mFavoritesList.add(new AndroidFlavour(ID, flavour, version));
}
loadAndroidFlavours();
} else {
hideList();
}
}
I'm stuck. Please help with explanation. Regards!

Hacky way of refreshing CursorAdapter cursor

Trying to refresh a ListView populated by CursorAdapter, I would have thought something like this would work:
SimpleCursorAdapter listAdapter;
#Override
protected void onRestart() {
super.onRestart();
listAdapter.swapCursor(listAdapter.getCursor());
}
In that it swaps cursor from old one to new one and the ListView gets the new data. Why doesn't this way work and what is the alternative?
Slightly more information, swapCursor() is supposed to return the old cursor but in the above it returns null.
EDIT:
I know why it doesn't work. swapCursor() checks if the new cursor and the old cursor are the same, if it is then it does nothing:
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
And changeCursor calls swapCursor so can't use that. Now to hack that into working...

Cursor Loaders filtering, old data shown in for a split second

I have a weird behavior in my App I'm struggling to understand.
I have 2 lists (A & B), the initial view is list A with a textBox on top that filters the results of list B.
I use cursor loaders, adapters and a listview to implement these 2 lists.
When the user enters a keyword to search, I restart Loader B with the entered keyword and load it into the adapter and set the listview to display adapter B.
Everything runs fine BUT instead of displaying the results right away, a wrong behavior happens: The full List B is shown for a split second, then the filtered results shows.
Of course this creates a very bad UX (user experience), what I want to achieve here is the filtered results to be shown right away without the split second view of the full list B.
I really want to understand why this behavior is occurring... after all this implementation style of querying is the one used in the official example of the cursor loaders :)
The code is too big but these are the most relevant parts:
This is the onTextChange that produces the bad behaviour:
#Override
public void onTextChanged(CharSequence s, int start, int before, int count)
{
getLoaderManager().restartLoader(LOADER_ID_2, null, this);
}
These are the cursorLoader methods:
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id== LOADER_ID_1)
{
//calculate and return cursorLoader
}
else
{
if (mFilter == null || mFilter.length() == 0)
{
return new CursorLoader(this, uri, CAdapter.PROJECTION, null,null,null);
}
//else
// do some calculations
return new CursorLoader(this,uri, CAdapter.PROJECTION, whereStmt, whereArgs, orderBy);
}
}
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId()== LOADER_ID_2)
{
mCAdapter.swapCursor(cursor);
}
else if (loader.getId()== LOADER_ID_1)
{
MatrixCursor newCursor = new MatrixCursor(DAdapter.PROJECTION);
if (cursor.moveToFirst()) {
do {
// basically I'm grouping filtering the returned cursor and populating the new matrix cursor with the filtered data because I didn't find a better way to filter and group results (get distinct rows)
}while (cursor.moveToNext());
}
mDAdapter.changeCursor(newCursor);
//I used change cursor because the cursor loader will only handle closing the "cursor" and not the "newCursor" I created and put into the adapter, I hope this doesn't backfire :)
}
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId()== LOADER_ID_2 )
mCAdapter.swapCursor(null);
else if (loader.getId()== LOADER_ID_1 )
mDAdapter.swapCursor(null);
}
Try this;
public Loader<Cursor> onCreateLoader(int id, Bundle args)
{
Loader<Cursor> cursorLoader
if (id== LOADER_ID_1)
{
//calculate and return cursorLoader
If(cursorLoader == null)
{
return new CursorLoader(this,uri,
CAdapter.PROJECTION, whereStmt, whereArgs, orderBy);
}//end inner if
}// end if
else if (mFilter == null || mFilter.length() == 0)
{
return new CursorLoader(this, uri, CAdapter.PROJECTION, null,null,null);
}// end else if
}// end method

How does a cursor used by a RemoteViewService get deactivated

I get ACRA exception reports from some users that the cursor which supplies data to my appwidget (RemoteViewService) is deactivated/closed. It never happens to me in person, but it happens enough where it's a bit of an issue.
Here's the code to my RemoteViewService:
public static class ListItemService extends RemoteViewsService {
public RemoteViewsFactory onGetViewFactory(final Intent intent) {
return new RemoteViewsFactory() {
private MyCursor cursor;
public void onCreate() {
// Nothing
}
public synchronized void onDestroy() {
if (this.cursor != null)
this.cursor.close();
}
public synchronized RemoteViews getViewAt(int position) {
// Here I read from the cursor and it crashes with
// the stack trace below
}
public int getCount() {
return ((this.cursor != null) ? this.cursor.getCount() : 0);
}
public int getViewTypeCount() {
return 1;
}
public boolean hasStableIds() {
return true;
}
public long getItemId(int position) {
return position;
}
public RemoteViews getLoadingView() {
return null;
}
public synchronized void onDataSetChanged()
{
if (this.cursor != null)
this.cursor.close();
this.cursor = getApplicationCntext().getContentResolver().query(myUri, null, null, null, null);
}
};
The stack trace varies from platform version to platform version. For example, I get the following on 4.0.3:
android.database.StaleDataException: Attempting to access a closed CursorWindow.Most probable cause: cursor is deactivated prior to calling this method.
On 2.3, I get a:
java.lang.IllegalStateException: attempt to re-open an already-closed object: android.database.sqlite.SQLiteQuery
I cannot, for the life of me, figure out who or what closes the cursor on me, other than from onDestroy() and onDataSetChanged(). Some users had reported that they weren't actively working with app when the crash happened.
I suspected maybe multiple calls to the ContentProvider return the same cursor and when I display my UI which uses the same query, they step on each other. But this doesn't appear to be the case as the cursor objects are different.
Any ideas?
Looks to me like a sync issue between multiple threads, one closing the previous cursor and one immediately accessing it thereafter.
You may want to consider closing the cursor right when you first can, such as a onPause event.
Also - you can add a safety precaution as checking cursor.isClosed() before you access it again. You can also add some synchronisation to your code.
A helper method that fetches the new cursor with a local var and only once it's done, replaces the previous one and closes it may be a quicker solution in the meantime.
From AOSP doc;
This interface provides random read-write access to the result set
returned by a database query. Cursor implementations are not required
to be synchronized so code using a Cursor from multiple threads should
perform its own synchronization when using the Cursor.
From Content provider basics,
The ContentResolver.query() client method always returns a Cursor containing the columns specified by the query's projection for the rows that match the query's selection criteria. A Cursor object provides random read access to the rows and columns it contains. Using Cursor methods, you can iterate over the rows in the results, determine the data type of each column, get the data out of a column, and examine other properties of the results. Some Cursor implementations automatically update the object when the provider's data changes, or trigger methods in an observer object when the Cursor changes, or both.
Try to add this.cursor = null on onDestory() method
public synchronized void onDestroy() {
if (this.cursor != null){
this.cursor.close();
this.cursor = null
}
}

Can someone explain to me on runQueryOnBackgroundThread?

Can someone just explain to me what is runQueryOnBackgroundThread as I already read through some sources but still not understand with it?
#Override
public Cursor runQueryOnBackgroundThread(CharSequence constraint){
FilterQueryProvider filter = getFilterQueryProvider();
if (filter != null){
return filter.runQuery(constraint);
}
Uri uri = Uri.withAppendedPath(
ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI, Uri.encode(constraint.toString()));
return content.query(uri, CONTACT_PROJECTION, null, null, null);
}
A handle of my Activity in the Adapter and the runQuery call in the filter makes a call to startManagingCursor on the Activity whenever the runQuery is called. This is not ideal because a background thread is calling startManagingCursor and also there could be a lot of cursors remaining open until the Activity is destroyed.
I added the following to my Adapter which has a handle on the Activity it is used within
#Override
public void changeCursor(Cursor newCursor) {
Cursor oldCursor = getCursor();
super.changeCursor(newCursor);
if(oldCursor != null && oldCursor != newCursor) {
// adapter has already dealt with closing the cursor
activity.stopManagingCursor(oldCursor);
}
activity.startManagingCursor(newCursor);
}
This makes sure that the current cursor used by the adapter is also managed by the activity. When the cursor is closed by the adapter management by the activity is removed. The last cursor held by the adapter will be closed by the activity by way of it still be managed by the activity.

Categories

Resources