I'm creating a MergeCursor like so:
#Override
public Cursor loadInBackground() {
Log.d(LOG, "loading data in background");
ContentResolver cr = getContext().getContentResolver();
Cursor people = getContext().getContentResolver().query(PeopleProvider.CONTENT_URI, null, null, null, null);
people.setNotificationUri(cr, PeopleProvider.CONTENT_URI);
Cursor jobs = getContext().getContentResolver().query(JobProvider.CONTENT_URI, null, null, null, null);
jobs.setNotificationUri(cr, JobsProvider.CONTENT_URI);
Cursor horses = getContext().getContentResolver().query(HorseProvider.CONTENT_URI, null, null, null, null);
horses.setNotificationUri(cr, HorseProvider.CONTENT_URI);
Cursor[] c = new Cursor[3];
c[0] = people;
c[1] = jobs;
c[2] = horses;
MergeCursor mc = new MergeCursor(c);
return mc;
}
Then when I add new rows to the corresponding db tables:
"PeopleProvider.java"
#Override
public Uri insert(Uri uri, ContentValues values) {
Log.d(LOG, "Insert into People");
long id = database.insertOrThrow(DatabaseConstants.TABLE_PEOPLE, null, values);
getContext().getContentResolver().notifyChange(uri, null);
return ContentUris.withAppendedId(uri, id);
}
I also tried:
"WhereDataIsPresentedFragment.java"
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
Log.d(LOG, "onActivityResult()");
switch(requestCode) {
case NEW_PEOPLE_ACTIVITY:
Log.d(LOG, "Returned with newly added person");
if (resultCode==Activity.RESULT_OK) {
mListViewAdapter.notifyDataSetChanged();
getActivity().getContentResolver().notifyChange(PeopleProvider.CONTENT_URI, null);
}
break;
}
}
All methods are being called (confirmed via Log.d), so I'm not sure why the cursors are not being updated when the notifcations are being fired. Any insight into making this work would be greatly appreciated. I've had no issues with regular Cursors in the past, but no luck with this MergeCursor business.
So the way I got this working (whether it's right or not) is by further subclassing my AsyncTaskLoader class:
#Override
protected void onStartLoading() {
forceLoad();
}
Essentially by adding the forceLoad(); to the onStartLoading() method the everything now works as expected. I was able to remove all the additional calls to notifyDataSetChanged() and notifications to URIs. That single line fixed everything. If someone has a better solution please do let me know, but for now this is what I got.
Related
I did research on how to use ContentProviders and Loaders from this tutorial
How I see it:
We have an Activity with ListView, SimpleCursorAdapter and CursorLoader. We also implement ContentProvider.
In an Activity we can call getContentResolver().insert(URI, contentValues); via a button click.
In our implementation of ContentProvider, at the end of insert() method, we call getContentResolver().notifyChange(URI, null); and our CursorLoader will receive message that it should reload data and update UI. Also if we use FLAG_REGISTER_CONTENT_OBSERVER in SimpleCursorAdapter it will also receive message and its method onContentChanged() will be called.
So our ListView will be updated if we insert, update or delete data.
Activity.startManagingCursor(cursor); is deprecated, cursor.requery() deprecated, so I do not see any practice sense from cursor.setNotificationUri().
I looked into setNotificationUri() method's source code and saw that it calls mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver) inside the method. Also CursorLoader does the same. Finally cursor will receive message and the following method will be called inside Cursor:
protected void onChange(boolean selfChange) {
synchronized (mSelfObserverLock) {
mContentObservable.dispatchChange(selfChange, null);
// ...
}
}
But I can not make sense of this.
So my question is: why should we call cursor.setNotificationUri() in query() method of our ContentProvider implementation?
If you call Cursor.setNotificationUri(), Cursor will know what ContentProvider Uri it was created for.
CursorLoader registers its own ForceLoadContentObserver (which extends ContentObserver) with the Context's ContentResolver for the URI you specified when calling setNotificationUri.
So once that ContentResolver knows that URI's content has been changed [ this happens when you call getContext().getContentResolver().notifyChange(uri, contentObserver); inside ContentProvider's insert(), update() and delete() methods ] it notifies all the observers including CursorLoader's ForceLoadContentObserver.
ForceLoadContentObserver then marks Loader's mContentChanged as true
CursorLoader registers observer for the cursor, not to the URI.
Look into CursorLoader's source code below. Notice that CursorLoader registers contentObserver to the cursor.
/* Runs on a worker thread */
#Override
public Cursor loadInBackground() {
synchronized (this) {
if (isLoadInBackgroundCanceled()) {
throw new OperationCanceledException();
}
mCancellationSignal = new CancellationSignal();
}
try {
Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection,
mSelectionArgs, mSortOrder, mCancellationSignal);
if (cursor != null) {
try {
// Ensure the cursor window is filled.
cursor.getCount();
cursor.registerContentObserver(mObserver);
} catch (RuntimeException ex) {
cursor.close();
throw ex;
}
}
return cursor;
} finally {
synchronized (this) {
mCancellationSignal = null;
}
}
The Cursor needs to call method setNotificationUri() to register mSelfObserver to the uri.
//AbstractCursor.java
public void setNotificationUri(ContentResolver cr, Uri notifyUri, int userHandle) {
synchronized (mSelfObserverLock) {
mNotifyUri = notifyUri;
mContentResolver = cr;
if (mSelfObserver != null) {
mContentResolver.unregisterContentObserver(mSelfObserver);
}
mSelfObserver = new SelfContentObserver(this);
mContentResolver.registerContentObserver(mNotifyUri, true, mSelfObserver, userHandle); // register observer to the uri
mSelfObserverRegistered = true;
}
}
Inside the contentProvider's insert, update, delete methods, you need to call getContext().getContentResolver().notifyChange(uri, null); to notify change to the uri observers.
So if you don't call cursor#setNotificationUri(), your CursorLoader will not receive notification if data underlying that uri changes.
I use one URI for the cursor adaptor.
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = new Bundle();
Uri uri = TemperatureContract.SensorEntry.buildSensorID0AddressUri(mDeviceAddress);
args.putParcelable("URI", uri);
getSupportLoaderManager().initLoader(0, args, this);
}
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (args != null) {
Uri mUri = args.getParcelable("URI");
return new CursorLoader(this,
mUri,
null, // projection
null, // selection
null, // selectionArgs
null); // sortOrder
} else {
return null;
}
}
On another class, I use a different URI to change the database contents. To have my view updated, I had to change the default implementation of the data provider's update method. The default implementation only notifies the same URI. I have to notify another URI.
I ended up by calling the notifyChange() twice on my data provider class, on the update method:
#Override
public int update(
Uri uri, ContentValues values, String selection, String[] selectionArgs) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
int rowsUpdated;
switch (match) {
case ...:
break;
case SENSOR_BY_ID_AND_ADDRESS:
String sensorId = TemperatureContract.SensorEntry.getSensorIdFromUri(uri);
String sensorAddress = TemperatureContract.SensorEntry.getSensorAddressFromUri(uri);
rowsUpdated = db.update(
TemperatureContract.SensorEntry.TABLE_NAME, values, "sensorid = ? AND address = ?", new String[]{sensorId, sensorAddress});
if (rowsUpdated != 0) {
Uri otheruri = TemperatureContract.SensorEntry.buildSensorID0AddressUri(sensorAddress);
getContext().getContentResolver().notifyChange(otheruri, null);
}
break;
case ...:
break;
default:
throw new UnsupportedOperationException("Unknown uri: " + uri);
}
if (rowsUpdated != 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return rowsUpdated;
I did the same for the insertand delete methods.
I have an SQLiteDatabase whose data is managed by a Content Provider. I'm using a tabbed layout. The first tab has the ability to add rows to the database, whereas the second tab shows items from the database. As I add items to the database from the first tab, the changes should be reflected when I move to the other tab.
Data is being added to the database correctly, and upon first opening of the app, all the current data (and anything new added in a previous version of the app) will appear. However, adding new items to the database is not reflected in the ListFragment.
#Override
public void onClick(View v) {
if(v == addSale) {
Item item = new Item(rn(), null, 100);
data.add(item);
total += item.getAmount();
} else if(v == save) {
for(Item i: data) {
ContentValues cv = new ContentValues();
cv.put(DatabaseProvider.COLUMN_COST, i.getAmount());
cv.put(DatabaseProvider.COLUMN_ITEM, i.getItem());
cv.put(DatabaseProvider.COLUMN_PERSON, i.getPerson());
this.getActivity().getContentResolver().insert(DatabaseProvider.CONTENT_URI, cv);
}
total = 0;
data.clear();
} else if(v == clear) {
data.clear();
total = 0;
}
items.notifyDataSetChanged();
totalView.setText(Item.format(total));
}
Here is where I add the items to the database specifically with these lines:
ContentValues cv = new ContentValues();
cv.put(DatabaseProvider.COLUMN_COST, i.getAmount());
cv.put(DatabaseProvider.COLUMN_ITEM, i.getItem());
cv.put(DatabaseProvider.COLUMN_PERSON, i.getPerson());
this.getActivity().getContentResolver().insert(DatabaseProvider.CONTENT_URI, cv);
As I said earlier, items are put into the database correctly, so I'm reasonably sure that this is correct.
Here is the insert method of my DatabaseProvider
#Override
public Uri insert(Uri uri, ContentValues initialValues) {
if (sUriMatcher.match(uri) != TABLE) {
throw new IllegalArgumentException("Invalid URI: " + uri);
}
if (initialValues == null) {
initialValues = new ContentValues();
}
SQLiteDatabase db = dbHelper.getWritableDatabase();
long rowId = db.insert(TABLE_SALES, COLUMN_COST, initialValues);
if (rowId > 0) {
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
getContext().getContentResolver().notifyChange(uri, null);
return newUri;
}
throw new SQLException("Failed to insert row into: " + uri);
}
From the various tutorials and other SO questions, it seems as if
getContext().getContentResolver().notifyChange(uri, null);
is the key to getting it to update, and it's there, and is called. But nothing updates.
Finally, my list fragment that display all of the data.
package org.worldsproject.android.barcode;
import org.worldsproject.android.barcode.database.DatabaseProvider;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.CursorAdapter;
import android.support.v4.widget.SimpleCursorAdapter;
import com.actionbarsherlock.app.SherlockListFragment;
public class RunningTotal extends SherlockListFragment implements
LoaderManager.LoaderCallbacks<Cursor> {
public static final String TAG = "Running Total";
public static final int RUNNING_TOTAL_ID = 1;
private SimpleCursorAdapter adapter;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] uiBindFrom = { DatabaseProvider.COLUMN_PERSON };
int[] uiBindTo = { R.id.titled };
adapter = new SimpleCursorAdapter(
getActivity().getApplicationContext(), R.layout.list_title,
null, uiBindFrom, uiBindTo,
CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
setListAdapter(adapter);
getLoaderManager().initLoader(RUNNING_TOTAL_ID, null, this);
}
#Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
String[] projection = { DatabaseProvider.COLUMN_ID,
DatabaseProvider.COLUMN_PERSON};
return new CursorLoader(getActivity(), DatabaseProvider.CONTENT_URI,
projection, null, null, null);
}
#Override
public void onLoadFinished(Loader<Cursor> arg0, Cursor cursor) {
// TODO Auto-generated method stub
adapter.swapCursor(cursor);
}
#Override
public void onLoaderReset(Loader<Cursor> arg0) {
// TODO Auto-generated method stub
adapter.swapCursor(null);
}
}
It's nearly a direct clone of http://mobile.tutsplus.com/tutorials/android/android-sdk_loading-data_cursorloader/ and at the moment just displays the name column of each row in the database. It shows previous entries, but as I've said, doesn't update. What step am I missing to get it to update?
I think your
getContext().getContentResolver().notifyChange(uri, null);
may be fine.
I can't see your query function for your DatabaseProvider, but did you remember to set the notificationUri for the cursor you are returning in your query function?
In your query() function of the DatabaseProvider, you should set the notification Uri for the cursor, or else the cursor won't get a notification even if you do a notifyChange():
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// blah blah
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
I had a similar problem with a Fragment using a view pager. I realized that I didn't have a ContentObserver registered, so all I did was add this code to the onResume()
getActivity().getContentResolver().registerContentObserver(sUri, true, myObserver);
and have myObserver declared as a member variable:
new ContentObserver(new Handler()) {
#Override
public void onChange(boolean selfChange) {
getActivity().getSupportLoaderManager().restartLoader(); // With appropriate loader args here
}
});
And make sure to unregister is in the onPause();
That seemed to fix my problem, assuming that the ContentProvider is calling notifyChange correctly (It looks like you are, though).
Hope this helps you out!
I think the error is here:
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, rowId);
getContext().getContentResolver().notifyChange(uri, null);
return newUri;
You should call the notifyChange method with the uri of the element you just added, which is newUri.
So the notify becomes:
getContext().getContentResolver().notifyChange(newUri, null);
I am building an app that follows the IOSched way of retrieving data, with the exception of the fact that I thought I would use CursorLoader rather than ContentObserver:
I have also been referring to Reto's android-protips-location which does use CursorLoader and the logic flow is quite similar to IOSched, thus:
initLoader → startService (serviceIntent) → handleIntent → insert into DB → notifyChange → onLoadFinished → update UI
What I am expecting to see happen is CursorLoader return a Cursor once an insert has been performed on the database.
Currently, the fragment onActivityCreated calls initLoader and runs query on the ContentProvider this returns the Cursor for that point in time, with current data.
However, it appears that onLoadFinished is not being triggered when I perform a refresh. Logs show that delete and insert on the ContentProvider are performed, yet viewing the log shows that notifyChange is dispatched on insert.
// in my Fragment:
#Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(0, null, this);
refreshWelcome();
}
public void refreshWelcome() {
Intent i = new Intent(getActivity(), SyncService.class);
i.setAction(SyncService.GET_WELCOME);
getActivity().startService(i);
}
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri queryUri = AppContract.Welcome.CONTENT_URI;
String[] projection = new String[] { Welcome.WELCOME_FIRST_NAME };
String where = null;
String[] whereArgs = null;
String sortOrder = null;
// create new cursor loader
CursorLoader loader = new CursorLoader(getActivity(), queryUri, projection, where, whereArgs, sortOrder);
return loader;
}
//in AppProvider (which extends ContentProvider)
#Override
public Uri insert(Uri uri, ContentValues values) {
final SQLiteDatabase db = mOpenHelper.getWritableDatabase();
final int match = sUriMatcher.match(uri);
switch (match) {
case WELCOME: {
long rowId = db.insertOrThrow(Tables.WELCOME, null, values);
if (rowId > 0) {
getContext().getContentResolver().notifyChange(uri, null);
return uri;
}
}
}
return null;
}
As far I know, you receive the cursor in onLoadFinished; onCreateLoader returns a Loader< Cursor>.
I do it this way, setting the notification Uri for the cursor just after received it. It works fine for me.
#Override
public void onLoadFinished(Loader<Cursor>loader, Cursor data) {
Log.v(DEBUG_TAG, "onLoadFinished");
data.setNotificationUri(getActivity().getContentResolver(),yourURI);
((SimpleCursorAdapter) getListAdapter()).swapCursor(data);
if (data.getCount() == 0) {
Toast.makeText(getActivity(), "no elements",Toast.LENGTH_SHORT).show();
return;
}
<}
I want to show the items queried from database in the listview with SimpleCursorAdapter.
For example, there may be 20,000 items in the database. I want to just load 100 items(_id : 1-100) queried instead of load all items, when scrolling in the end of listview, load another 100 items(_id : 101-200) queried, how to achieve it? Any suggestion is welcome, thanks.
Relative codes are as follows:
protected void onCreate(Bundle savedInstanceState) {
mCursor = managedQuery(CONTENT_URI, PROJECTION, null, null, "_id DESC");
mAdapter = new SimpleCursorAdapter(this,R.layout.list_content, mCursor, keys, values);
setListAdapter(mAdapter);
}
In my defined listview, i want to load more items by query database.
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
int lastItem = firstVisibleItem + visibleItemCount - 1;
if (mListAdapter != null) {
if ((lastItem == mListAdapter.getCount()-1) && (mRefreshState != REFRESHING)) {
mFooterView.setVisibility(View.VISIBLE);
mRefreshState = REFRESHING;
new Handler().postDelayed(new Runnable() {
public void run() {
//execute the task , i want to load more items by query database
RefreshListView(LOADING_STORED_INFO);
}
}, DEFAULT_DELAY_TIMER);
}
}
}
In the AsyncTask loading data, i do the query operation.
protected Integer doInBackground(Integer... params)
{
Uri uri = ContentUris.withAppendedId(CONTENT_URI, mCursor.getInt(0)-1);
cursor = managedQuery(uri, PROJECTION, null, null, "_id DESC");
return (0 == params[0]) ? 1 : 0;
}
#Override
protected void onPostExecute(Integer result)
{
mAdapter.changeCursor(cursor);//is this OK?
mAdapter.notifyDataSetChanged();
/*
if (1 == result)
{
mListView.setSelection(1);
}
else
{
mListView.setSelection(mCount-1);
}*/
// Call onRefreshComplete when the list has been refreshed.
mListView.onRefreshComplete(result);
super.onPostExecute(result);
}
Use the LIMIT statement in the SQL query in this way:
SELECT your_column FROM your_table ORDER BY your_order LIMIT limit_skip, limit_count
Then you can use a OnScrollListener to retrieve the index of the first visible cell and the number of visible cells so you can increment limit_skip and limit_count coherently.
Instead of the generic AsyncTask use a CursorLoader and implement LoaderManager.LoaderCallbacks<Cursor> as follow:
public Loader<Cursor> onCreateLoader(int id, Bundle args){
String orderBy = "_id DESC"
if(args != null){
orderBy += " LIMIT " + args.getInt("LIMIT_SKIP") + "," + args.getInt("LIMIT_COUNT");
}
return new CursorLoader(this /*context*/, CONTENT_URI, PROJECTION, null, null, orderBy);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data){
listAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader){
listAdapter.swapCursor(null);
}
Then, in onCreate(), pass null as cursor to new SimpleCursorAdapter() and create the CursorLoader in this way:
getLoaderManager().initLoader(0, null, this /*LoaderCallbacks<Cursor>*/);
Then, in onScroll(), reset everytime the loader in this way:
Bundle args = new Bundle();
args.putInt("LIMIT_SKIP", limit_skip_value);
args.putInt("LIMIT_COUNT", limit_count_value);
getLoaderManager().restartLoader(0, args, this /*LoaderCallbacks<Cursor>*/);
I've implemented a custom Adapter for a ExpandableListView which I extended from the CursorTreeAdapter class. Everything is working as expected.
But I'm wondering if there's pattern or some kind of best practice on how to asynchronously query the database in the getChildrenCursor() method of the adapter class. At the moment I'm passing my SQLiteOpenHelper class to the constructor of my adapter and use it in getChildrenCursor() to query the database synchronously on the UI thread.
You could also use a CursorLoader instead of subclassing AsyncTask to asynchronously query a provider.
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id != -1) {
// child cursor
return new CursorLoader(getActivity(), childrenUri,
CHILDREN_PROJECTION, selection, selectionArgs, sortOrder);
} else {
// group cursor
return new CursorLoader(getActivity(), groupsUri,
GROUPS_PROJECTION, selection, null, sortOrder);
}
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
int id = loader.getId();
if (id != -1) {
// child cursor
if (!data.isClosed()) {
try {
mAdapter.setChildrenCursor(id, data);
} catch (NullPointerException e) {
Log.w("TAG",
"Adapter expired, try again on the next query: "
+ e.getMessage());
}
}
} else {
// group cursor
mAdapter.setGroupCursor(data);
}
}
public void onLoaderReset(Loader<Cursor> loader) {
int id = loader.getId();
if (id != -1) {
// child cursor
mAdapter.setChildrenCursor(id, null);
} else {
// group cursor
mAdapter.setGroupCursor(null);
}
}
And in your adapter class you can override the getChildrenCursor() method like this:
protected Cursor getChildrenCursor(Cursor groupCursor) {
// Given the group, we return a cursor for all the children within that group
int id = groupCursor.getInt(groupCursor
.getColumnIndex(ContactsContract.Groups._ID));
mActivity.getLoaderManager().initLoader(id, null,mFragment);
return null;
}
getChildrenCursor says:
If you want to asynchronously query a
provider to prevent blocking the UI,
it is possible to return null and at a
later time call setChildrenCursor(int,
Cursor).
So, in getChildrenCursor(), start an AsyncTask and return null. In the onPostExecute() method call setChildrenCursor()