Fragments, ContentProviders and cursors on orientation change - android

Please help me understand what's happening here.
I have two fragments (A and B) in tabs reading data from different tables, via a CursorLoader and aContentProvider, in a Sqlite-database. With different URIs I can change which table the ContentProvider is quering.
I works as expected when switch between the tabs A and B, unless I switch to B, rotate and switches back to A the wrong cursor is returned. The cursor from fragment B is returned instead of a cursor for fragment A with makes the listView in fragment A to cause a crash. In some way the cursor seems to be reused on a rotation.
Why is this happening and how can I prevent that the wrong cursor is returned?
This is what I have in both fragment A and B. Tried to assing a loader id with no success.
public void onResume() {
super.onResume();
getLoaderManager().restartLoader(mLoaderId, null, this);
}
My ContentProvider looks like this:
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
Cursor cursor = null;
switch (uriMatcher.match(uri)) {
case ALL_NEWS:
queryBuilder.setTables(NewsDb.SQLITE_TABLE);
cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
break;
case SINGLE_PLACE:
queryBuilder.setTables(PlacesDb.SQLITE_TABLE);
String id = uri.getPathSegments().get(1);
queryBuilder.appendWhere(PlacesDb.KEY_ID + "=" + id);
cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
return cursor;
}

When using CursorLoader, data changes are automatically observed, so you should definitely remove the restartLoader call in your onResume(). If implemented properly, you should only have to call initLoader in onActivityCreated of the Fragment.
LoaderManager IDs are only scoped to the Fragment, so you should be using a static constant ID. If the Loaders are handled in the Fragments, themselves, it's perfectly fine for every Fragment to have the same Loader ID (even if they're managed by the same Activity).
So in each Fragment:
private static final int LOADER_ID = 0;
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// ...
getLoaderManager().initLoader(LOADER_ID, null, this);
}

Related

OnLoadFinished suddenly not called anymore after update

I have a fragment with a list of items, each item has an editable quantity. When I edit the quantity, the database is updated and I receive a new cursor in the fragment. This all works well, except when you update the quantity a number of times, the onLoadFinished method is not called anymore. This can happen after 5 updates or after 200 updates: I can't reproduce the bug on command. The update is executed in the database because when I close and open that screen, I can see the updated quantity. Nothing out of the ordinary happens in the code: onLoaderReset is not called, notifyChange gets called on the correct Uri in the contentprovider. Has anyone ever seen this issue?
When the quantity needs to get updated:
ContentValues cv = new ContentValues();
cv.put(CartTable.COLUMN_COUNT, quantity);
String selection = CartTable.COLUMN_ID + " = " + cartItemId;
contentResolver.update(CartContentProvider.CONTENT_URI, cv, selection, null);
The CartContentProvider:
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
{
int rowsUpdated;
switch (uriMatcher.match(uri))
{
case CART_ITEMS:
rowsUpdated = this.database.update(CartTable.TABLE_NAME, values, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
if(rowsUpdated > 0)
{
this.getContext().getContentResolver().notifyChange(CONTENT_URI, null);
this.getContext().getContentResolver().notifyChange(CONTENT_URI_PRODUCTS, null);
this.getContext().getContentResolver().notifyChange(CONTENT_URI_PRODUCTS_PHOTOS, null);
this.getContext().getContentResolver().notifyChange(CONTENT_URI_PHOTOS, null);
}
return rowsUpdated;
}
When I create the CursorLoader:
return new CursorLoader(this.getView().getContext(), CartContentProvider.CONTENT_URI_PRODUCTS, projection, null, null, null);
So for example, I edit the quantity 20 times and 20 times onLoadFinished gets called with a new cursor. The 21st time and all times after that, the update happens but onLoadFinished is not called anymore.
When extra code is required, let me know!
Thanks in advance.
Try using the CursorLoader like this:
return new CursorLoader(this.getActivity(), CartContentProvider.CONTENT_URI_PRODUCTS, projection, null, null, null);
use the fragment's Activity context, Also check where are you implementing the LoaderCallbacks interface, in the activity or in the fragment?

CursorLoader not updating after notifyChange call

I have registered a CursorLoader, but it is not receiving updates from my ContentProvider
The sequence of events is:
In a Fragment, register the CursorLoader with:
getLoaderManager().initLoader(LOADER_FAVS_ID, null, this);
Note I am using the support library version ,so this method is android.support.v4.app.Fragment.getLoaderManager()
The CursorLoader is registered, and a Cursor is loaded in the onLoadFinished:
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
Log.i(TAG, "onLoadFinished");
switch (loader.getId()) {
case LOADER_FAVS_ID:
Log.i(TAG,
"cursor notification uri: " + cursor.getNotificationUri());
mCursorAdapter.swapCursor(cursor);
break;
}
}
Which Logs cursor notification uri: content://com.myapp.mylocation/db_locations, for example. This is because I made sure to call cursor.setNotificationUri(getContext().getContentResolver(), uri); before returning the Cursor from my ContentProvider.
Also note that my content provider returns a MergeCursor.
Some time later, a call is made to update() in my ContentProvider, and the following lines are executed:
Log.i(TAG,
"Notifying uri: " + uri.toString());
getContext().getContentResolver().notifyChange(
uri, null);
Which Logs Notifying loc uri: content://com.myapp.mylocation/db_locations, the same uri as above.
But onLoadFinished is never called, and my Cursor is never updated. I believe I have followed the advice I can find, all of which is basically this. Why else would onLoadFinished not be called after all of this?
Solved it, but since I haven't seen this documented, here's my solution.
I was returning a MergeCursor from my ContentProvider, basically just concatenating a list of cursors. I had
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Generate an array of Cursors
MergeCursor mergCursor = new MergeCursor(cursorArray);
// notify potential listeners
mergCursor.setNotificationUri(getContext().getContentResolver(), uri);
return mergCursor;
}
And my CursorLoader was never receiving the notification. However, as MergeCursor is basically just an Array of Cursor, you need to set the notification uri on each cursor in your MergeCursor.
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// Generate an array of Cursors
// set the notification uris...
for (Cursor cursor : cursorArray) {
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
MergeCursor mergCursor = new MergeCursor(cursorArray);
return mergCursor;
}
Now everything works as expected!

Cursorloader not refreshing when underlying data changes

I have a content provider, a content resolver, and a cursor loader. The loader is used to indirectly populate a listview (ie not a simple cursor adapter, rather an array adapter, since I need to use the cursor's results to gather other data).
When I change the underlying data, the listview does not re-populate as the onLoadFinished(Loader<Cursor>, Cursor) call back is not called.
As suggested while I'm writing this, there are a lot of questions on this issue.
eg
CursorLoader not updating after data change
And all the questions point out two things:
In your content provider query() method, you need to tell the cursor about notifications a'la
c.setNotificationUri(getContext().getContentResolver(), uri);
In your content provider insert/update/delete method, you need to notify on the URI:
getContext().getContentResolver().notifyChange(uri, null);
I'm doing those things.
I'm also NOT closing any of the cursors I get back.
Note, my content provider lives in a separate app (think of it as a content provider app -- no launcher main activity). A com.example.provider APK, and the com.example.app is calling (in the content resolver) via the content://com.example.provider/table URI etc. The content resolver (dbhelper) lives in a library project (com.example.appdb) that the activity links in. (This way, multiple projects can use the dbhelper via linking, and all content providers are installed via single APK)
I have turned on debugging in the loader manager, and can see where I force a refresh after the data changes (ie the loader being restarted and previous being marked inactive), but nothing that says anything is happening automatically -- rather, in response to my force refresh.
Any ideas why my loader isn't being refreshed ?
-- EDIT --
The loader create:
Log.d(TAG, "onCreateLoader ID: " + loaderID);
// typically a switch on the loader ID, and then
return dbHelper.getfoo()
Where getfoo() returns a cursor loader via:
return new CursorLoader(context, FOO_URI, foo_Fields, foo_query, foo_argArray, foo_sort );
Loader Finish takes the cursor and populates a table (and does some processing) -- nothing fancy there.
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
Log.d(TAG, "onLoadFinished id: " + loader.getId());
// switch on loader ID ..
FillTable(cursor, (FooAdapter) foo_info.listview.getAdapter();
Loader Reset clears the table.
public void onLoaderReset(Loader<Cursor> loader) {
Log.d(TAG, "onLoaderReset id: " + loader.getId());
// normally a switch on loader ID
((FooAdapter)foo_info.listview.getAdapter()).clear();
The content provider does:
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor cursor;
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
SQLiteDatabase db = dbHelper.getReadableDatabase();
switch (sUriMatcher.match(uri)) {
case FOO:
qb.setTables(FOO);
qb.setProjectionMap(FooProjectionMap);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
c.setNotificationUri(getContext().getContentResolver(), uri);
return c;
}
Then insert/update is similar:
public Uri insert(Uri uri, ContentValues initialValues) {
ContentValues values;
if (initialValues != null) {
values = new ContentValues(initialValues);
} else {
values = new ContentValues();
}
String tableName;
Uri contentUri;
switch (sUriMatcher.match(uri)) {
case FOO:
tableName = FOOTABLE;
contentUri = FOO_URI;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long rowId = db.insert(tableName, null, values);
if (rowId > 0) {
Uri objUri = ContentUris.withAppendedId(contentUri, rowId);
getContext().getContentResolver().notifyChange(objUri, null);
return objUri;
}
throw new SQLException("Failed to insert row into " + uri);
}
I see that you aren't calling swapCursor or changeCursor in your onLoadFinished and onLoaderReset. You need to do that for your adapter to access the data loaded into the new cursor.
in onLoadFinished, call something like:
mAdapter.swapCursor(cursor)
In onLoaderReset, call this to remove references to the old cursor:
mAdapter.swapCursor(null)
Where mAdapter is your listView's adapter.
More info: http://developer.android.com/guide/components/loaders.html#onLoadFinished
So, for anyone who looks this up and has this problem:
Make certain that the URI you register the cursor on, and the URI you notify the cursor on are the same.
In my case, I was using a different URI to query than was used in other code that modified the underlying table. There wasn't an easy fix to make notification work, so I kept resetting the loader in OnResume() as the solution.
This is not a direct answer to this specific problem, but may help others in a similar situation. In my case, I had a join and even though I used the full tablename.columnname in the projection and query. It ended up using the wrong ID values anyway. So be sure to check your ContentProvider or SQL syntax.

Is it a good practice to use rawQuery in ContentProvider?

I am using my custom ContentProvider to communicate with sqlite database. I would like to display on a list (using ListFragment), data that comes from two tables (with many to many relation). The only solution I can think of for such case is to use rawQuery. And the questions is, if it is a good practice, or should I solve this in some other way?
Example of tables:
Table A: ID, COLUMN_FROM_A
Table B: ID, COLUMN_FROM_B
Joining table AB: ID, FK_ID_A, FK_ID_B
Example of overridden query method in ContentProvider:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
Cursor cursor = null;
int uriType = URIMatcher.match(uri);
switch (uriType) {
case TABLE_A_URI:
queryBuilder.setTables("TABLE_A");
cursor = queryBuilder.query(databaseHelper.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);
break;
case TABLE_B_URI:
queryBuilder.setTables("TABLE_B");
cursor = queryBuilder.query(databaseHelper.getReadableDatabase(), projection, selection, selectionArgs, null, null, sortOrder);
break;
case TABLE_JOIN_A_B_URI:
cursor = databaseHelper.getReadableDatabase().rawQuery("select a.COLUMN_FORM_A, b.COLUMN_FROM_B from TABLE_A a, TABLE_B b, TABLE_AB ab where ab.FK_ID_A=a.ID and ab.FK_ID_B=b.ID", null);
break;
default:
throw new IllegalArgumentException("Unknown URI");
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
It's a good and common practice, very appropriate in this case.
I don't foresee any problems, we have used it in many apps.

CursorLoader with rawQuery

I'm looking into implementing CursorLoader in my app but I'm having a small issue that it seems that there isn't a way to just a pass a raw query to the CursorLoader constructor.
I maybe missing something in the documentation (and google), so if anybody can point me to a simple way to run a raw query with a CursorLoader class I would appreciate it. Otherwise I will have to probably create my own CursorLoader class with the necessary functionality, which I'm trying to avoid.
it seems that there isn't a way to just a pass a raw query to the CursorLoader constructor.
That is because CursorLoader works with content providers, and content providers do not support rawQuery().
so if anybody can point me to a simple way to run a raw query with a CursorLoader class I would appreciate it.
That is impossible, sorry. You are welcome to create your own AsyncTaskLoader that hits a SQLite database and supports rawQuery(). In fact, I will probably write one of these later this year, if I don't see where anyone has beaten me to it.
Raw query is not supported directly, but you can do a dirty hack: from your code call
getContentResolver().query(RAWQUERY_CONTENT_URI, null, rawquery, args, null);
and implement content provider like
#Override
public synchronized Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
int uriType = sURIMatcher.match(uri);
switch (uriType)
{
case RAW_QUERY:
return dbHelper.getReadableDatabase().rawQuery(selection, selectionArgs);
}
[...]
}
**For Custom Search using Content provider **
Change Cursor Loader as Follow (in onCreateLoader )
return new CursorLoader(
getActivity(), // Context
PRODUCT.CONTENT_URI, // URI
PROJECTION, // Projection
PRODUCT.PRODUCT_NAME+ " like ?", // Selection
new String[]{"%" + mCurFilter + "%"}, // Selection args
PRODUCT.PRODUCT_NAME + " asc");
In your Provider Change Accordingly
//C is Cursor object
switch (uriMatch) {
case ROUTE_PRODUCT_ID:
// Return a single entry, by ID.
String id = uri.getLastPathSegment();
builder.where(PRODUCT._ID + "=?", id);
c = builder.query(db, projection, sortOrder);
assert ctx != null;
c.setNotificationUri(ctx.getContentResolver(), uri);
return c;
// break;
case ROUTE_PRODUCT:
// Return all known entries.
builder.table(PRODUCT.PRODUCT_TABLE_NAME)
.where(selection, selectionArgs);
c = builder.query(db, projection, sortOrder);
assert ctx != null;
c.setNotificationUri(ctx.getContentResolver(), uri);
return c;
You can implement your own CursorLoader with raw query. This is the source of the original CursorLoader: https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/CursorLoader.java

Categories

Resources