I'm using Loader and Content Provider to fill a listview with data from my database.
My problem is that, if I use the line below in my Content Provider to get data from database, the query and onLoadFinished is executed very fast (no problem to fill the listview):
Cursor c = this.mDb.rawQuery("SELECT * FROM " + DATABASE_TABLE , null);
However, If I change the line to :
Cursor c = this.mDb.rawQuery("SELECT DISTINCT D._id, D.Name, D.Icon FROM Plain A, Test B, Exercise C, Group D WHERE A.ID=1 AND B.PlainID=A._id AND B.TestID=C._id AND C.ExerciseID=D._id", null);
The query continues to run fast, but the application takes approximately 2s to call onLoadFinished and fill the listview.
why this delay if the cursor already executed query?
My Loader:
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.d(TAG, "onCreateLoader");
Uri uri = ContentProviderGroup.CONTENT_URI;
String[] projection = { Group.KEY_ROW_ID, Group.KEY_NAME, Group.KEY_ICON };
CursorLoader cursorLoader = new CursorLoader(this, // Parent activity context
uri, // Table to query
projection, // Projection to return
null, // No selection clause
null, // No selection arguments
null); // Default sort order
Log.d(TAG, "END onCreateLoader");
return cursorLoader;
}
My Content Provider (query function):
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
Cursor cursor;
if(uriMatcher.match(uri) == CODE){
Log.d(TAG, "CALL SELECT");
cursor = mCustomerDB.getGroup(selection, selectionArgs);
Log.d(TAG, "END SELECT");
return cursor;
}else{
return null;
}
}
My DBAdapter (getGroup function):
public Cursor getGroup(String selection, String[] selectionArgs){
Log.d(TAG, "SELECT INIT");
/////// HERE I USE the rawQuery /////////
Log.d(TAG, "SELECT END");
return c;
}
Thank You
Related
I am trying to retrieve the distinct values from my database using my contentprovider query and CursorLoader. While the CursorLoader does not allow a distinct specification, I discovered the setDistinct method that can be added to a querybuilder for adding this specification. I am not retrieving the desired result and curious as to why. My query looks like below
#Override
public Cursor query(Uri uri, String[] projection,
String selection, String[] selectionArgs, String sortOrder) {
// create SQLiteQueryBuilder for querying flower table
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(Flower.TABLE_NAME);
queryBuilder.setDistinct(true);
switch (uriMatcher.match(uri)) {
case OneFlower: // contact with specified id will be selected
queryBuilder.appendWhere(
Flower._ID + "=" + uri.getLastPathSegment());
break;
case CONTACTS: // all contacts will be selected
break;
default:
throw new UnsupportedOperationException(
getContext().getString(R.string.invalid_query_uri) + uri);
}
Cursor cursor = queryBuilder.query(dbHelper.getReadableDatabase(),
projection, selection, selectionArgs, null, null, sortOrder);
// configure to watch for content changes
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
while my CursorLoader looks like this
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
switch (id) {
case LOADER_ID:
return new CursorLoader(
getContext(),
DatabaseDescription.Flower.CONTENT_URI,
FROM_COLUMNS,
COLUMN_LOCATION + "<> ''",
null,
COLUMN_LOCATION + " ASC"
);
default:
if (BuildConfig.DEBUG)
throw new IllegalArgumentException("no id handled!");
return null;
}
}
I have a sqlite database with "label" and "idea" tables in my Android app. made a foreign key on idea table as idea_label with int values that connected to label_table on its _id.
I use Loader to load my Cursor on the mainActivity that loads my idea table from the provider. As obvious it load idea_label int (But what I seek is to load the value from label_table which sets in label_body).
My loader on mainActivity class
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
String[] projection = {
DatabaseContract.IdeaEntry._ID,
DatabaseContract.IdeaEntry.COLUMN_IDEA_NAME,
DatabaseContract.IdeaEntry.COLUMN_IDEA_DESCRIPTION,
DatabaseContract.IdeaEntry.COLUMN_IDEA_DATE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_LABEL,
DatabaseContract.IdeaEntry.COLUMN_IDEA_ICON,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_ACTIVE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_FAVORITE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_DONE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_ARCHIVED,
DatabaseContract.IdeaEntry.COLUMN_IDEA_ORDER,
};
return new CursorLoader(this,
DatabaseContract.IdeaEntry.CONTENT_URI_IDEA,
projection,
null, // selection
null, // selectionArgs
DatabaseContract.IdeaEntry.COLUMN_IDEA_ORDER // order
);
}
That calls this section on my provider class
#Nullable
#Override
public Cursor query(#NonNull Uri uri, #Nullable String[] projection, #Nullable String selection, #Nullable String[] selectionArgs, #Nullable String sortOrder) {
SQLiteDatabase database = mDatabaseHelper.getReadableDatabase();
Cursor cursor;
int match = sUriMatcher.match(uri);
switch (match){
case IDEAS:
cursor = database.query(DatabaseContract.IdeaEntry.IDEA_TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
Is there a way? trigger another loader? or my implementation is wrong in this case? any way or help that direct me to the right direction, will be very appreciated.
With SQLiteQueryBuilder you can join tables in setTables method and then just specify label_body column of label table in projection.
String[] projection = {
DatabaseContract.IdeaEntry._ID,
....
DatabaseContract.LabelEntry.COLUMN_LABEL_BODY,
};
...
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables("IDEA JOIN LABEL ON IDEA.IDEA_LABEL = LABEL._ID");
builder.query(database, projection, selection, selectionArgs, null, null, sortOrder);
Thanks to #gar_r to help and guide me , I did like this with DatabaseContract constants:
#Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
String[] projection = {
DatabaseContract.IdeaEntry.IDEA_TABLE_NAME + "." + DatabaseContract.IdeaEntry._ID,
DatabaseContract.IdeaEntry.COLUMN_IDEA_NAME,
DatabaseContract.IdeaEntry.COLUMN_IDEA_DESCRIPTION,
DatabaseContract.IdeaEntry.COLUMN_IDEA_DATE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_LABEL,
DatabaseContract.IdeaEntry.COLUMN_IDEA_ICON,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_ACTIVE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_FAVORITE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_DONE,
DatabaseContract.IdeaEntry.COLUMN_IDEA_IS_ARCHIVED,
DatabaseContract.IdeaEntry.COLUMN_IDEA_ORDER,
DatabaseContract.LabelEntry.COLUMN_LABEL_BODY,
};
return new CursorLoader(this,
DatabaseContract.IdeaEntry.CONTENT_URI_IDEA,
projection,
null, // selection
null, // selectionArgs
DatabaseContract.IdeaEntry.COLUMN_IDEA_ORDER // order
);
}
That calls this section on my provider class
#Nullable
#Override
public Cursor query(#NonNull Uri uri, #Nullable String[] projection, #Nullable String selection, #Nullable String[] selectionArgs, #Nullable String sortOrder) {
SQLiteDatabase database = mDatabaseHelper.getReadableDatabase();
Cursor cursor;
int match = sUriMatcher.match(uri);
switch (match){
case IDEAS:
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(DatabaseContract.IdeaEntry.IDEA_TABLE_NAME + " JOIN " + DatabaseContract.LabelEntry.LABEL_TABLE_NAME
+ " ON " + DatabaseContract.IdeaEntry.COLUMN_IDEA_LABEL + " = " + DatabaseContract.LabelEntry.LABEL_TABLE_NAME + "." + DatabaseContract.LabelEntry._ID);
cursor = builder.query(database, projection, selection, selectionArgs, null, null, sortOrder);
break;
I am using a a CursorLoader and ContentProvider to read data from SQLite DB. The 'drugs' table has about 300,000 rows. I have created index on 'name' column in the 'drugs' table.
I have a SearchFragment where I search for drugs based on user query as follows:
public void update(String query, SearchView mSearchView, String mSearchType) {
mQuery = query;
Bundle b = new Bundle();
b.putString("query", mQuery);
mLoaderManager.restartLoader(Constants.DRUG_LOADER, b, this);
Log.d(TAG, "Searching for " + mQuery);
}
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Log.d(TAG, "onCreateLoader");
return new CursorLoader(getActivity(), DrugsProvider.DRUGS_URI,
null,
ReapDbContract.Drugs.NAME + " like ?",
new String[]{args.getString("query") + "%"},
null
);
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
Log.d(TAG, "onLoadFinished");
mAdapter.setCursor(c);
mAdapter.notifyDataSetChanged();
Log.d(TAG, "Done query for " + mQuery);
}
This is relevant part of DrugsProvider which runs the query:
#Override
public Cursor query(Uri uri, String[] projection, String where, String[] values, String sortOrder) {
switch (sUriMatcher.match(uri)) {
case DRUGS_TABLE:
Cursor c = mDbHelper.query(Drugs.TABLE_NAME, projection, where, values, sortOrder);
Log.d(TAG, "query done");
return c;
case DRUG_ROW:
String id = uri.getLastPathSegment();
return mDbHelper.getRow(Drugs.TABLE_NAME, id);
default:
throw new IllegalArgumentException("Invalid URI: " + uri);
}
}
The DrugsHelper actually implements the query:
public Cursor query(String tableName, String[] projection, String where, String[] values, String sortOrder) {
Log.d(TAG, "query: start: " + Arrays.toString(values));
SQLiteDatabase db = getWritableDatabase();
Log.d(TAG, "query: db");
Cursor c = db.query(tableName, projection, where, values, null, null, sortOrder);
Log.d(TAG, "query: done: " + Arrays.toString(values));
return c;
}
The debug logs clearly tell that the actual DB query gets executed in a few milliseconds, but there is a delay of almost 500ms between the end of query and call to onLoadFinished. This is making the search extremely sluggish.
Sometimes there is also a delay of about 500ms between call to onCreateLoader and when the provider actually runs the query. Here is a sample log:
================================================================
01-07 10:23:13.618 29288-29288/in.workcell.pos D/SearchFragment: onCreateLoader
01-07 10:23:13.627 29288-29288/in.workcell.pos D/SearchFragment: Searching for pari
01-07 10:23:13.632 29288-29524/in.workcell.pos D/DrugsHelper: query: start: [pari%]
01-07 10:23:13.632 29288-29524/in.workcell.pos D/DrugsHelper: query: db
01-07 10:23:13.632 29288-29524/in.workcell.pos D/DrugsHelper: query: done: [pari%]
01-07 10:23:13.632 29288-29524/in.workcell.pos D/DrugsProvider: query done
01-07 10:23:14.098 29288-29288/in.workcell.pos D/SearchFragment: onLoadFinished
01-07 10:23:14.098 29288-29288/in.workcell.pos D/SearchFragment: Done query for pari
================================================================
The DrugsProvider was done at 10:23:13.632, but onLoadFinished got called at 10:23:14.098, delay of 466ms! How can I debug what is causing this delay ?
I am using a searchView to get a query to search database with a string, the Cursor does not return as null but is empty.
The call to search from my search activity (which is a ListActivity implementing SearchView.OnQueryTextListener):
#Override
public boolean onQueryTextChange(String newText) {
// TODO Auto-generated method stub
//handleIntent(intent);
Log.d("LIST ACTIVITY SEARCH", "onQueryTextCahanged called: "+ newText);
Cursor c = data.getWordMatches(newText, null);
Log.d("LISTACTIVITY", "CURSOR QUERY ROWS/COLS = "+ c.getCount()+" "+c.getColumnCount());
//now set bind cursor data to list view using custim ItemAdpter class
adapter = new ItemAdapter(ViewListOfTests.this, c);
this.setListAdapter(adapter);
return false;
}
And the search method in SQLite Databasetaking the String query from method above:
/*
* methods to search database with String query form searchable widget intent
* in ViewListOfDives ListActivity search bar. Each search returns a cursor which is set to adpter
* in ViewListOfYesys List Activity
*/
public Cursor getWordMatches(String query, String[] columns){
String selection = database.KEY_TESTNAME + " MATCH ?";
String[] selectionArgs = new String[] {query+"*"}; //wildcard *
return query(selection, selectionArgs, columns);
}//end getWordMatches
private Cursor query(String selection, String[] selectionArgs,
String[] columns) {
// return cursor form params passed getWordMatches(Styring query) from search widget ListActivity
SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
builder.setTables(DATABASE_TABLE);
dbHelperObject = new DbHelper(ourContext);
Cursor cursor = builder.query(dbHelperObject.getReadableDatabase(), columns, selection, selectionArgs, null, null, null);
if(cursor == null){
return null;
}else if(!cursor.moveToFirst()){
cursor.close();
return null;
}
return cursor;
}//end query
The else if code executes here returning an empty cursor :
else if(!cursor.moveToFirst()){
cursor.close();
return null;
Any input appreciated!
Ciaran
instead of this :
if(cursor == null){
return null;
}else if(!cursor.moveToFirst()){
cursor.close();
return null;
}
return cursor;
use this:
if(cursor == null){
return null;
}else{
cursor.moveToFirst();
}
return cursor;
Got it, seems i was passing the wrong parameter to the curosr.rawQuery().
I was passing a ref to dataBase helper object getReadableDB call:
Cursor cursor = builder.query(mDatabaseOpenHelper.getReadableDatabase(),
columns, selection, selectionArgs, null, null, null);
When I replaced this parameter with the database object it worked fine:
SQLiteDatabase ourDataBase = ourDataBase = SQLiteDatabase.openDatabase(myPath, null, SQLiteDatabase.OPEN_READONLY);
Cursor cursor = builder.query(ourDataBase, columns, selection, selectionArgs, null, null, null);
The training documentation stated pass the former mDatabaseOpenHelper.getReadableDatabase() but this did not seem to work, not why.http://developer.android.com/training/search/search.html
I am trying to get email ids of uses contacts. For that I am using Cursor Loader. There is one problem I am getting duplicate email ids also. How to remove email duplicacy. Should I use raw query "SELECT DISTINCT" instead of using CursorLoader or there is some other solution?
#Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, ContactsContract.CommonDataKinds.Email.DATA};
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP +"='1' AND " + Email.DATA +" IS NOT NULL AND " + Email.DATA +" != \"\" " ;
//showing only visible contacts
String[] selectionArgs = null;
return new CursorLoader(this, ContactsContract.CommonDataKinds.Email.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
I recently ran into this problem. It appears that the CursorLoader does not have an implementation of "DISTINCT". My workaround adds a few lines to the onLoadFinish method and extends the BaseAdapter to accept a List parameter:
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
String projection[] = {
CommonDataKinds.Phone._ID,
CommonDataKinds.Phone.DISPLAY_NAME,
};
String select = "((" + CommonDataKinds.Phone.DISPLAY_NAME + " NOTNULL) and " + CommonDataKinds.Phone.HAS_PHONE_NUMBER + " > 0)";
String sort = CommonDataKinds.Phone.DISPLAY_NAME + " ASC";
CursorLoader loader = new CursorLoader(
mContext,
CommonDataKinds.Phone.CONTENT_URI,
projection,
select,
null,
sort
);
return loader;
}
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
List<String> displayNames = new ArrayList<String>();
cursor.moveToFirst();
while(!cursor.isAfterLast()){
String name = cursor.getString(cursor.getColumnIndex(CommonDataKinds.Phone.DISPLAY_NAME));
if(!displayNames.contains(name))
displayNames.add(name);
cursor.moveToNext();
}
mAdapter.swapCursor(displayNames);
}
Here is my BaseAdapter class:
public class AdapterAddContacts extends BaseAdapter{
private List<String> mData = new ArrayList<String>();
private Context mContext;
public AdapterAddContacts(Context context,List<String> displayNames){
mData = displayNames;
mContext = context;
}
#Override
public int getCount() {
if(mData != null)
return mData.size();
else
return 0;
}
#Override
public Object getItem(int pos) {
return mData.get(pos);
}
#Override
public long getItemId(int id) {
return id;
}
#Override
public View getView(int pos, View convertView, ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.entry_add_contacts,parent,false);
String data = mData.get(pos);
TextView textName = (TextView)view.findViewById(R.id.my_contacts_add_display_name);
textName.setText(data);
textName.setTag(data);
return view;
}
public void swapCursor(List<String> displayNames){
mData = displayNames;
this.notifyDataSetChanged();
}
You should be able to modify this specifically for your needs.
Inspired by #mars, I have a solution that does not need a modification of the adapter. The idea is to delete the duplicates of the cursor; as there is no way to do it, we create a new cursor whithout the duplicates.
All the code is in onLoadFinished:
#Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
MatrixCursor newCursor = new MatrixCursor(PROJECTION); // Same projection used in loader
if (cursor.moveToFirst()) {
String lastName = "";
do {
if (cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)).compareToIgnoreCase(lastName) != 0) {
newCursor.addRow(new Object[]{cursor.getString(0), cursor.getString(1), cursor.getString(2) ...}); // match the original cursor fields
lastName =cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
}
} while (cursor.moveToNext());
}
mContactsAdapter.swapCursor(newCursor);
}
I used a small hack in my project - an SQL injection, like that:
#Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
this,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
new String[] {
"DISTINCT "+ MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME},
null, null, null);
}
This code returns only bundle names and their IDs from Gallery.
So, I'd rewrite your code like that:
#Override
public Loader<Cursor> onCreateLoader(int arg0, Bundle arg1) {
String[] projection = new String[] {
"DISTINCT " + ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Email.DATA};
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP +"='1' AND " + Email.DATA +" IS NOT NULL AND " + Email.DATA +" != \"\" " ;
//showing only visible contacts
String[] selectionArgs = null;
return new CursorLoader(this, ContactsContract.CommonDataKinds.Email.CONTENT_URI, projection, selection, selectionArgs, sortOrder);
}
You can put setDistinct in your content provider.
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
...
final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setDistinct(true);
If you are worried about performance and don't want to play around with cursor again in onLoadFinished(), then there is a small hack
I combined following two solutions from SO.
select distinct value in android sqlite
CursorLoader with rawQuery
And here is my working solution:
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String tableName;
/*
* Choose the table to query and a sort order based on the code returned
* for the incoming URI.
*/
switch (uriMatcher.match(uri)) {
case NOTIFICATION:
tableName = NOTIFICATIONS_TABLE_NAME;
break;
case NOTIFICATION_TIMESTAMP:
Cursor cursor = db.query(true, NOTIFICATIONS_TABLE_NAME, projection, selection, selectionArgs, TIMESTAMP, null, sortOrder, null);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
case DOWNLOAD:
tableName = DOWNLOADS_TABLE;
break;
default:
throw new IllegalArgumentException("Unknown URI " + uri);
}
if (selection != null) {
selection = selection + "=?";
}
Cursor cursor = db.query(tableName, projection, selection, selectionArgs, null, null, sortOrder);
// Tell the cursor what uri to watch, so it knows when its source data
// changes
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
If you see in this case Table name is same is first 2 cases but i created a dummy Uri to achieve this. May not be a very good approach but works perfectly.
I found a solution
Use DISTINCT keyword in selection Array.
String[] projection = new String[] { ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME, "DISTINCT" + ContactsContract.CommonDataKinds.Email.DATA};