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.
Related
in onLoadFinished() i use myadapter.swapCuesor(cursor) and i wonder if it calls newView in the adapter afterwards? its not written in the api of android's adapter so im asking it here. if not so how the adapter update itself?
When you are using a CursorLoader, the Cursor is managed for you. The only thing you have to do is implement the following three methods:
// Called when a new Loader needs to be created
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
return new CursorLoader(this, ContactsContract.Data.CONTENT_URI,
PROJECTION, SELECTION, null, null);
}
// Called when a previously created loader has finished loading
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
// Called when a previously created loader is reset, making the data unavailable
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
mAdapter.swapCursor(null);
}
You don’t have to open and close the Cursor yourself, the loader will do this for you. This is the most important reason why you have to use swapCursor, it doesn’t close the Cursor when you swap it with another Cursor.
public Cursor swapCursor(Cursor newCursor) {
if (newCursor == mCursor) {
return null;
}
Cursor oldCursor = mCursor;
if (oldCursor != null) {
if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
}
mCursor = newCursor;
if (newCursor != null) {
if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
mDataValid = true;
// notify the observers about the new cursor
notifyDataSetChanged();
} else {
mRowIDColumn = -1;
mDataValid = false;
// notify the observers about the lack of a data set
notifyDataSetInvalidated();
}
return oldCursor;
}
ChangeCursor on the other hand, first swaps the current Cursor with the new one and then closes it for you. If you use this method with your CursorLoader, your app may crash sometimes.
public void changeCursor(Cursor cursor) {
Cursor old = swapCursor(cursor);
if (old != null) {
old.close();
}
}
I am using a SQLite database and a ContentProvider to fill a ListFragment. The problem is that ListFragment is not getting refreshed after I add a item. The ListFragment is empty. I have to close and reopen the app to show the added item in the list.
I try to update it like this:
public class RoomListFragment extends ListFragment implements LoaderManager.LoaderCallbacks<Cursor> {
//adapter using SQLite and ContentProvider to fill ListFragment
private SimpleCursorAdapter dataAdapter;
//needed for create room dialog
private EditText enter_room;
private static View textEntryView;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//display ActionBar items
setHasOptionsMenu(true);
// TODO: replace with a real list adapter.
displayListView();
}
#Override
public void onResume() {
super.onResume();
//Starts a new or restarts an existing Loader in this manager
getLoaderManager().restartLoader(0, null, this);
}
#Override
public void onDestroy() {
super.onDestroy();
}
private void displayListView() {
// The desired columns to be bound
String[] columns = new String[] {
Database.KEY_GROUPADDRESS,
Database.KEY_NAME,
Database.KEY_DPT
};
// the XML defined views which the data will be bound to
int[] to = new int[] {
R.id.groupaddress,
R.id.name,
R.id.dpt,
};
// create an adapter from the SimpleCursorAdapter
dataAdapter = new SimpleCursorAdapter(
getActivity(),
R.layout.device_info,
null,
columns,
to,
0);
//set SimpleCursorAdapter to ListFragmentAdapter
setListAdapter(dataAdapter);
//Ensures a loader is initialized and active.
getLoaderManager().initLoader(0, null, this);
}
// This is called when a new Loader needs to be created.
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String[] projection = {
Database.KEY_ROWID,
Database.KEY_GROUPADDRESS,
Database.KEY_NAME,
Database.KEY_DPT};
CursorLoader cursorLoader = new CursorLoader(getActivity(),
MyContentProvider.CONTENT_URI, projection, null, null, null);
return cursorLoader;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
dataAdapter.swapCursor(data);
dataAdapter.notifyDataSetChanged();
if (isResumed()) {
setListShown(true);
} else {
setListShownNoAnimation(true);
}
}
#Override
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
dataAdapter.swapCursor(null);
}
I add a item with this code:
//Handle OnClick events on ActionBar items
#Override
public boolean onOptionsItemSelected(MenuItem item) {
// handle item selection
switch (item.getItemId()) {
case R.id.menu_add:
//Toast.makeText(getActivity(), "Click", Toast.LENGTH_SHORT).show();
LayoutInflater factory = LayoutInflater.from(getActivity());
//textEntryView is an Layout XML file containing text field to display in alert dialog
textEntryView = factory.inflate(R.layout.dialog_add_room, null);
//get the control from the layout
enter_room = (EditText) textEntryView.findViewById(R.id.enter_room);
//create Dialog
final AlertDialog.Builder alert1 = new AlertDialog.Builder(getActivity());
//configure dialog
alert1.setTitle("Raum hinzufügen:").setView(textEntryView)
.setPositiveButton("Hinzufügen",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
String roomname = enter_room.getText().toString();
Log.d("Insert: ", "Inserting ..");
ContentValues values = new ContentValues();
//TODO Richtige Spalte für Raumname verwenden
values.put(Database.KEY_NAME, roomname);
getActivity().getContentResolver().insert(MyContentProvider.CONTENT_URI, values);
dataAdapter.notifyDataSetChanged();
}
}).setNegativeButton("Abbrechen",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
//cancel dialog
}
});
alert1.show();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
My ContentProvider:
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
public class MyContentProvider extends ContentProvider{
private MyDatabaseHelper dbHelper;
private static final int ALL_COUNTRIES = 1;
private static final int SINGLE_COUNTRY = 2;
// authority is the symbolic name of your provider
// To avoid conflicts with other providers, you should use
// Internet domain ownership (in reverse) as the basis of your provider authority.
private static final String AUTHORITY = "de.mokkapps.fixknxdemo.contentprovider";
// create content URIs from the authority by appending path to database table
public static final Uri CONTENT_URI =
Uri.parse("content://" + AUTHORITY + "/countries");
// a content URI pattern matches content URIs using wildcard characters:
// *: Matches a string of any valid characters of any length.
// #: Matches a string of numeric characters of any length.
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, "countries", ALL_COUNTRIES);
uriMatcher.addURI(AUTHORITY, "countries/#", SINGLE_COUNTRY);
}
// system calls onCreate() when it starts up the provider.
#Override
public boolean onCreate() {
// get access to the database helper
dbHelper = new MyDatabaseHelper(getContext());
return false;
}
//Return the MIME type corresponding to a content URI
#Override
public String getType(Uri uri) {
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
return "vnd.android.cursor.dir/vnd.com.as400samplecode.contentprovider.countries";
case SINGLE_COUNTRY:
return "vnd.android.cursor.item/vnd.com.as400samplecode.contentprovider.countries";
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
// The insert() method adds a new row to the appropriate table, using the values
// in the ContentValues argument. If a column name is not in the ContentValues argument,
// you may want to provide a default value for it either in your provider code or in
// your database schema.
#Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
long id = db.insert(Database.SQLITE_TABLE, null, values);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(CONTENT_URI + "/" + id);
}
// The query() method must return a Cursor object, or if it fails,
// throw an Exception. If you are using an SQLite database as your data storage,
// you can simply return the Cursor returned by one of the query() methods of the
// SQLiteDatabase class. If the query does not match any rows, you should return a
// Cursor instance whose getCount() method returns 0. You should return null only
// if an internal error occurred during the query process.
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Database.SQLITE_TABLE);
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
case SINGLE_COUNTRY:
String id = uri.getPathSegments().get(1);
queryBuilder.appendWhere(Database.KEY_ROWID + "=" + id);
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
return cursor;
}
// The delete() method deletes rows based on the selection or if an id is
// provided then it deleted a single row. The methods returns the numbers
// of records delete from the database. If you choose not to delete the data
// physically then just update a flag here.
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
case SINGLE_COUNTRY:
String id = uri.getPathSegments().get(1);
selection = Database.KEY_ROWID + "=" + id
+ (!TextUtils.isEmpty(selection) ?
" AND (" + selection + ')' : "");
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
int deleteCount = db.delete(Database.SQLITE_TABLE, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return deleteCount;
}
// The update method() is same as delete() which updates multiple rows
// based on the selection or a single row if the row id is provided. The
// update method returns the number of updated rows.
#Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
switch (uriMatcher.match(uri)) {
case ALL_COUNTRIES:
//do nothing
break;
case SINGLE_COUNTRY:
String id = uri.getPathSegments().get(1);
selection = Database.KEY_ROWID + "=" + id
+ (!TextUtils.isEmpty(selection) ?
" AND (" + selection + ')' : "");
break;
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
int updateCount = db.update(Database.SQLITE_TABLE, values, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return updateCount;
}
}
You should call notifyDataSetChanged(); from within your content providers' insert method and supply the URI as an argument.
At the point in time you are currently calling the notifyDataSetChanged(); method the insert may not have actually happened as the content provider call to insert will be handled asynchronously.
An example could look something like this
#Override
public Uri insert(Uri uri, ContentValues values) {
SQLiteDatabase sqlDB = mDB.getWritableDatabase();
int uriType = sURIMatcher.match(uri);
long id;
switch (uriType) {
case TEAMS:
id = sqlDB.replace(TeamModel.TEAM_TABLE_NAME, null, values);
break;
case CARS:
id = sqlDB.replace(CarModel.CAR_TABLE_NAME, null, values);
break;
case TEAM_ERRORS:
id = sqlDB.replace(TeamErrorModel.TEAMS_ERRORS_TABLE_NAME, null, values);
String teamId = values.get(TeamErrorModel.COL_TEAM_ID).toString();
String selection = TeamModel.COL_ID + " = ?";
String[] selectionArgs = {teamId};
setErrorFlagTeamModel(sqlDB, true, selection, selectionArgs);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null, false);
return Uri.parse(uri + "/" + id);
}
The first argument is the URI passed in to the insert method and will tell ALL adapters listening in on that particular uri to update their data.
The last argument (false) tells a sync adapter to ignore this change. I assume you are not using a sync adapter
All methods in your ContentProvider should call the notifyChange method in a similar way.
You may well find that the insert actually failed. so check that the records are actually being inserted.
UPDATE
As per comment below from #zapi
And you need to add cursor.setNotificationUri(contentresolver, uri)
inside the query method or the Cursor does not know for which uri
notification it has to listen
Since answering your question you have posted your content provider and I can now see that in fact as per the above quote this is in fact your missing link
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'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()