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.
Related
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?
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!
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);
}
i have implemented update() of ContentProvider and notifying to observer using getContext().getContentResolver().notifyChange(uri, null);
my obvious need is that whenever just one row is effected i want to notify with row specific uri, but could not find way to do so.
an additional query like "select id where selectionArgs" can do this but this will be a foolish way.
onchange(boolean, uri) get complete uri instead of specific row, easy to understand that this is because ContentProvider.update() is sending the same.
some code for more clarity
update() method of MyContentProvider
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
Log.d("TAG", "update " + uri.getPath());
int count = 0;
switch (uriMatcher.match(uri)) {
case BOOKS:
count = booksDB.update(DATABASE_TABLE, values, selection, selectionArgs);
break;
case BOOK_ID:
count = booksDB.update(DATABASE_TABLE, values,
_ID + " = " + uri.getPathSegments().get(1)
+ (!TextUtils.isEmpty(selection) ? " AND (" + selection + ')' : ""),
selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (count == 1) {
Cursor c = query(uri, new String[] { _ID }, selection, selectionArgs, null);
long rowId = Long.valueOf(c.getString(c.getColumnIndex(_ID)));
uri = ContentUris.withAppendedId(CONTENT_URI, rowId);
}
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
i will update table some how like
getContentResolver().update(MyContentProvider.CONTENT_URI, values1, MyContentProvider._ID+"<?", new String[]{"3"}));
frankly saying, code has barely related to question, just trying to give you some context
In your provider method, just return the uri with the id appended
#Override
public Uri insert(Uri uri, ContentValues values) {
Log.i(TAG, "insert " + uri);
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = URI_MATCHER.match(uri);
Uri returnUri;
switch (match) {
case MESSAGE: {
long _id = db.insert(MessageContract.MessageEntry.TABLE_NAME, null, values);
if (_id > 0)
returnUri = ContentUris.withAppendedId(MessageContract.MessageEntry.CONTENT_URI, _id);
else
throw new android.database.SQLException("Failed to insert row into " + uri);
break;
}
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
getContext().getContentResolver().notifyChange(returnUri, null);
return returnUri;
}
And register your observer with true for descendents.
getContentResolver().registerContentObserver(MessageContract.MessageEntry.CONTENT_URI, true, mContentObserver);
To get the id from a Uri you can use ContentUris.parseId(uri)
Unfortunately I'm not able to suggest easy solution (because I'm not aware of full code and updates You need to run), there's some ways we You could try (some of them I've implemented in mine applications):
Provide ids in ContentValues - this way looks not applicable for Your case and it needs loop with calls to notifyChange();
Provide specific Uri for requests with queries (only some specific apps needs many various queries in selection, usually it's much easier to include query parameter in Uri). After another part of the program get notification with that specific Uri it will be able to check if it's 'current item' was updated and act appropriately (e.g. simplest case with list of articles and one article open in separate activity; then You update list of articles in the background from server You might need to update currently open article also and so, need to know if it was updated). You should be able to check particular item on the side of the observer using just received Uri, because it (Uri) will contain parameter(s) You've used for query;
You can pass the ID via ContentValues, and append it to the notification url. This way you don't have to make a separate query.
#Override
public int update(#NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int rows = _database.update(getTableName(), values, selection, selectionArgs);
if (rows > 0) {
Uri itemUri = ContentUris.withAppendedId(uri, values.getAsLong(DatabaseModel.COLUMN_ID)); // DatabaseModel.COLUMN_ID is "_id"
getContext().getContentResolver().notifyChange(itemUri, null);
}
return rows;
}
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