ContentLoader not properly refreshing after database changes - android

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);

Related

What is cursor.setNotificationUri() used for?

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.

onLoadFinished() receives a null cursor

I'm trying to populate a ListView in my activity using a ContentProvider that I created, but when the onLoadFinished() method is called, it receives a null Cursor. (The CursorLoader is not null though).
I'm trying to debug the ContentProvider, but the debugger doesn't stop on the query() method (it stops when the URI doesn't match, when I fix that, it doesn't stop!!!). So without debug I'm having a hard time to find out why my cursor returns as null on the onLoadFinished() method.
Here is the code for the activity:
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SimpleCursorAdapter;
import android.view.Menu;
import android.widget.ListView;
public class PicturesListActivity extends FragmentActivity
implements LoaderManager.LoaderCallbacks<Cursor> {
SimpleCursorAdapter myAdapter;
String[] projection = {"_id", "name"};
String[] from = {"name"};
String orderBy = "year DESC";
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pictures_list);
final ListView listview = (ListView) findViewById(R.id.lv_pictures_list);
// The TextView in simple_list_item_1
int[] toViews = {android.R.id.text1};
// Prepare the loader. Either re-connect with an existing one, or start a new one.
getSupportLoaderManager().initLoader(0, null, this);
myAdapter = new SimpleCursorAdapter(this, android.R.layout.simple_list_item_1, null, from, toViews, 0);
listview.setAdapter(myAdapter);
}
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.pictures_list, menu);
return true;
}
#Override
// Called when a new Loader needs to be created (initLoader)
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = Uri.parse(DatabaseContentProvider.AUTHORITY + DatabaseContentProvider.TABLE_PATH_PICTURE);
CursorLoader cl = new CursorLoader(this, uri, projection, null, null, orderBy);
return cl;
}
#Override
// Called automatically when a previously created loader has finished loading
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
myAdapter.swapCursor(data);
}
#Override
// Called when a previously created loader is reset, making the data unavailable
public void onLoaderReset(Loader<Cursor> loader) {
myAdapter.swapCursor(null);
}
}
And here is the ContentProvider that I created:
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 DatabaseContentProvider extends ContentProvider {
//database
private DatabaseAssetHelper dbhelper;
//stuff
public static final String AUTHORITY = "com.frlnrl.myapp.DatabaseContentProvider";
public static final String TABLE_PATH_PICTURE = DatabaseContract.Pictures.TABLE_PICTURE;
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
//UriMatcher
private static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sURIMatcher.addURI(AUTHORITY, TABLE_PATH_PICTURE , 37);
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// TODO Auto-generated method stub
int numberOfLinesDeleted = 0;
return numberOfLinesDeleted;
}
#Override
public String getType(Uri uri) {
return null;
}
#Override
public Uri insert(Uri uri, ContentValues values) {
// TODO Auto-generated method stub
return null;
}
#Override
public boolean onCreate() {
dbhelper = new DatabaseAssetHelper(getContext());
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
SQLiteDatabase db = dbhelper.getReadableDatabase();
int uriReceived = sURIMatcher.match(uri);
switch (uriReceived) {
case 37:
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
queryBuilder.setTables(DatabaseContract.Pictures.TABLE_PICTURE);
break;
default:
throw new IllegalArgumentException("Unknown oh eu aqui URI");
}
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
#Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO Auto-generated method stub
int numberOfLineUpdated = 0;
return numberOfLineUpdated;
}
}
Does anyone got any idea why is returns a empty cursor?
First you should fix the CONTENT_URI for your path.
Change
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
to
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/" + TABLE_PATH_PICTURE);
You weren't including the path on your Uri.
Then when creating your cursor loader you can use the CONTENT_URI instead of trying to parse the Uri each time (Which you were doing wrong resulting in your cursor loader trying to query a Uri that didn't exist and returning a null cursor )
#Override
// Called when a new Loader needs to be created (initLoader)
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = DatabaseContentProvider.CONTENT_URI;
CursorLoader cl = new CursorLoader(this, uri, projection, null, null, orderBy);
return cl;
}

Android: Refresh ListFragement using ContentProvider and Loader

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

How to organize classes while making a database app with SQlite

well, the other post, i mean :
Should there be one SQLiteOpenHelper for each table in the database?
i was doubtful about having one or more Helpers for the application, well, the anwser was 1 helper, many columns.
now the second part, i'm actually making this tutorial:
http://www.vogella.com/articles/AndroidSQLite/article.html
but i would like to do it with more than 1 table on the database, ok, i've done the work and now i'm try to make the so called Data Access Object, "DAO". the question is the same, is better to have a single DAO, or is better to have one for each table (and so one for each table class) OR lastly, a single DAO class with all the "actions" i could use in the application... ?
this is what in the tutorial is called DAO:
package de.vogella.android.sqlite.first;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
public class CommentsDataSource {
// Database fields
private SQLiteDatabase database;
private MySQLiteHelper dbHelper;
private String[] allColumns = { MySQLiteHelper.COLUMN_ID,
MySQLiteHelper.COLUMN_COMMENT };
public CommentsDataSource(Context context) {
dbHelper = new MySQLiteHelper(context);
}
public void open() throws SQLException {
database = dbHelper.getWritableDatabase();
}
public void close() {
dbHelper.close();
}
public Comment createComment(String comment) {
ContentValues values = new ContentValues();
values.put(MySQLiteHelper.COLUMN_COMMENT, comment);
long insertId = database.insert(MySQLiteHelper.TABLE_COMMENTS, null,
values);
Cursor cursor = database.query(MySQLiteHelper.TABLE_COMMENTS,
allColumns, MySQLiteHelper.COLUMN_ID + " = " + insertId, null,
null, null, null);
cursor.moveToFirst();
Comment newComment = cursorToComment(cursor);
cursor.close();
return newComment;
}
public void deleteComment(Comment comment) {
long id = comment.getId();
System.out.println("Comment deleted with id: " + id);
database.delete(MySQLiteHelper.TABLE_COMMENTS, MySQLiteHelper.COLUMN_ID
+ " = " + id, null);
}
public List<Comment> getAllComments() {
List<Comment> comments = new ArrayList<Comment>();
Cursor cursor = database.query(MySQLiteHelper.TABLE_COMMENTS,
allColumns, null, null, null, null, null);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
Comment comment = cursorToComment(cursor);
comments.add(comment);
cursor.moveToNext();
}
// Make sure to close the cursor
cursor.close();
return comments;
}
private Comment cursorToComment(Cursor cursor) {
Comment comment = new Comment();
comment.setId(cursor.getLong(0));
comment.setComment(cursor.getString(1));
return comment;
}
}
Thanks in advance.
This is the class that will help your application to interact with your database. You should have only one DAO class with all methods you need. (Different methods for different tables.)
Check example below:
public void insertInTableA (String[]) //or any args
{
//Logic for table A row insertion
}
public void insertInTableB (String[]) //or any args
{
//Logic for table B row insertion
}
public void dltFromTableA (String where) //or any args
{
//Logic for table A row deletion
}
public void dltFromTableB (String where) //or any args
{
//Logic for table B row deletion
}
//Other function as per requirement

IntentService and CursorLoader Sychronisation issue

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;
}
<}

Categories

Resources