Is Android Cursor.moveToNext() Documentation Correct? - android

boolean android.database.Cursor.moveToNext() documentation says:
http://developer.android.com/reference/android/database/Cursor.html#moveToNext%28%29
Move the cursor to the next row.
This method will return false if the cursor is already past the last entry in the result set.
However, my book says to do the following to extract data from a cursor:
Cursor myCursor = myDatabase.query(...);
if (myCursor.moveToFirst()) {
do {
int value = myCursor.getInt(VALUE_COL);
// use value
} while (myCursor.moveToNext());
}
Who's right? These both can't be true. If you can't see the contradiction, imagine myCursor has 1 row returned from the query. The first call to getInt() will work, but then moveToNext() will return true because it is not "already" past the last entry in the result set. So now the cursor will be past the last entry and the second call to getInt() will do something undefined.
I suspect the documentation is wrong and should instead read:
This method will return false if the cursor is "already at" the last entry in the result set.
Must the cursor be already PAST (not AT) the last entry before the moveToNext() method returns false?
No Snark Please

A simpler idiom is:
Cursor cursor = db.query(...);
while (cursor.moveToNext()) {
// use cursor
}
This works because the initial cursor position is -1, see the Cursor.getPosition() docs.
You can also find usages of cursors in the Android source code itself with this Google Code Search query. Cursor semantics are the same in SQLite database and content providers.
References: this question.

Verbatim from the API:
Returns:
whether the move succeeded.
So, it means that:
Cursor in first row -> moveToNext() -> cursor in second row -> there's no second row -> return false
If you want the details, go to the source: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.3_r1/android/database/AbstractCursor.java#AbstractCursor.moveToNext%28%29
public final boolean moveToNext() {
return moveToPosition(mPos + 1);
}
public final boolean moveToPosition(int position) {
// Make sure position isn't past the end of the cursor
final int count = getCount();
if (position >= count) {
mPos = count;
return false;
}

I think I tend to stay away from solutions that are based on a hidden assumption like: I hope that sqlite never changes it api and that a cursor will always start at the first item.
Also, I have almost always been able to replace a while statement with a for statement. So my solution, shows what I expect the cursor to start at, and avoids using a while statement:
for( boolean haveRow = c.moveToFirst(); haveRow; haveRow = c.moveToNext() ) {
...
}
why is showing that a cursor needs to start at the first row, well 6 months down the line you might be debugging your own code, and will wonder why you didn't make that explicit so you could easily debug it.

It appears to be down to the Android implementation of AbstractCursor and it remains broken in Jellybean.
I implemented the following unit test to demonstrate the problem to myself using a MatrixCursor:
#Test
public void testCursor() {
MatrixCursor cursor = new MatrixCursor(new String[] { "id" });
for (String s : new String[] { "1", "2", "3" }) {
cursor.addRow(new String[] { s });
}
cursor.moveToPosition(0);
assertThat(cursor.moveToPrevious(), is(true));
cursor.moveToPosition(cursor.getCount()-1);
assertThat(cursor.moveToNext(), is(true));
assertThat(cursor.moveToPosition(c.getCount()), is(true));
assertThat(cursor.moveToPosition(-1), is(true));
}
All assertions fail, contrary to the documentation for moveToNext, moveToPrevious and moveToPosition.
Reading the code at API 16 for AbstractCursor.moveToPosition(int position) it appears to be intentional behaviour, ie the methods explicitly return false in these cases, contrary to the documentation.
As a side note, since the Android code sat on existing devices in the wild cannot be changed, I have taken the approach of writing my code to match the behaviour of the existing Android implementation, not the documentation. ie. When implementing my own Cursors / CursorWrappers, I override the methods and write my own javadoc describing the departure from the existing documentation. This way, my Cursors / CursorWrappers remain interchangeable with existing Android cursors without breaking run-time behaviour.

Cursor.moveToNext() returning a boolean is only useful if it will not move the cursor past the last entry in the data set. Thus, I have submitted a bug report on the documentation's issue tracker.
https://issuetracker.google.com/issues/69259484
It reccomends the following sentence:
"This method will return false if the current (at time of execution) entry is the last entry in the set, and there is no next entry to be had."

Related

Cursor#getCount is used to ensure content window is filled?

Below is a code snippet from an Android tutorial book I was following. loadInBackground gets a cursor and then does cursor.getCount() to "ensure that the content window is filled". What does this mean? The docs on getCount just say "returns number of rows in the cursor". I've Googled for this "ensure that the content window is filled" and there are quite a few snippets that do this, all with the same comment, but no explanation of why this is needed/how it works.
import android.content.AsyncTaskLoader;
import android.content.Context;
import android.database.Cursor;
public abstract class SQLiteCursorLoader extends AsyncTaskLoader<Cursor> {
private Cursor cursor;
public SQLiteCursorLoader(Context context) {
super(context);
}
protected abstract Cursor loadCursor();
#Override
public Cursor loadInBackground() {
Cursor cursor = loadCursor();
if (cursor != null) {
cursor.getCount(); // ensure that the content window is filled
}
return cursor;
}
}
As you know, Database returns a Cursor after a query. However, Cursor is only effectively filled with data when you try to read some information like: cursor.getCount() or cursor.moveToFirst() etc...
This is more evident during large queries.
For example, imagine that query below would return thousand of results:
Cursor cursor = db.rawQuery("select * from TABLE", null);
That statement however, does not take too much time to run...
However, when you finally call cursor.getCount() or cursor.moveToFirst(), for the first time, you may see some "lag" since the cursor is being effectively being filled with the data from database.
If you do that in Main UI Thread, you app may freeze for some seconds. Specially on low tier devices.
So, by calling that method, I believe that author is trying to ensure that data was fully loaded during loadInBackground(). This way, he ensure that data is loaded in Background an not in any other future method. This way, any future call to getCount() or moveToFirst() will be executed very quickly since the data was already loaded.
Anyway, it is not mandatory..

SQLite database insert is somehow vanishing?

I'm having a very strange issue on my Android app wherein when I am inserting a value to a DB table, the first entry is disappearing somehow. However, any subsequent entries are appearing fine.
To be a little more specific, part of my application allows users to create a simple log where they enter some text and when they save it, it shows up on a list of log entries. However, when I try to insert the very first entry to an empty table, that entry is not being displayed, nor does the database indicate there is any data when I query for a count.
Interestingly enough, when I look at the return of the database insert call (SQLiteDatabase.insert()) I see a valid row number returned. In fact, when I look at any log entry I've saved to the database, the row number is correctly incrementing. As per the docs, my understanding is that if a non-negative number is returned, the insert was successful.
Here is the code that takes the result of the EditText from my AlertDialog, creates a new log entry, and calls the insert method:
newPainLogEntryDialog.setPositiveButton("Save",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
//make new pain log entry
PainLog painLog = new PainLog();
painLog.setPainEntry(input.getText().toString());
painLog.setPainDateTime(Calendar.getInstance());
Database.init(PainLogTab.this.getActivity());
Database.createPainLog(painLog);
updatePainLogList();
//display success to user
Toast.makeText(PainLogTab.this.getActivity(),
"Log entry saved", Toast.LENGTH_SHORT).show();
}
});
The code for Database.createPainLog():
public static long createPainLog(PainLog painLog) {
ContentValues cv = new ContentValues();
cv.put(COLUMN_PAINLOG_ENTRY, painLog.getPainEntry());
cv.put(COLUMN_PAINLOG_DATETIME, painLog.getPainDateTimeString());
return getDatabase().insert(PAINLOG_TABLE, null, cv);
}
And the last call before the Toast message is updatePainLogList(), which gets all the DB entries:
public void updatePainLogList(){
Database.init(PainLogTab.this.getActivity());
final List<PainLog> painLogs = Database.getAllPainLogs();
painLogListAdapter.setPainLogs(painLogs);
Log.d(getClass().getSimpleName(), "number of painLogs found: " + painLogs.size());
getActivity().runOnUiThread(new Runnable() {
public void run() {
// reload content
PainLogTab.this.painLogListAdapter.notifyDataSetChanged();
if(painLogs.size() > 0){
getView().findViewById(android.R.id.empty).setVisibility(View.INVISIBLE);
}else{
getView().findViewById(android.R.id.empty).setVisibility(View.VISIBLE);
}
}
});
}
And for completion sake, the body of the getAll() and its accompanying method getCursor():
public static Cursor getPainLogCursor() {
String[] columns = new String[] {
COLUMN_PAINLOG_ID,
COLUMN_PAINLOG_ENTRY,
COLUMN_PAINLOG_DATETIME
};
return getDatabase().query(PAINLOG_TABLE, columns, null, null, null, null,
null);
}
public static List<PainLog> getAllPainLogs() {
List<PainLog> painLogs = new ArrayList<PainLog>();
Cursor cursor = Database.getPainLogCursor();
if (cursor.moveToFirst()) {
while (cursor.moveToNext()) {
PainLog painLog = new PainLog();
painLog.setId(cursor.getInt(IDX_PAINLOG_ID));
painLog.setPainEntry(cursor.getString(IDX_PAINLOG_ENTRY));
painLog.setPainDateTime(cursor.getString(IDX_PAINLOG_DATETIME));
painLogs.add(painLog);
}
}
cursor.close();
return painLogs;
}
Now with some code I can explain what debugging steps I have taken thus far. As mentioned above, when I look at the return of the DB insert, I get a positive, non-zero number. However, when I try to print the number of logs in the immediately following update method (no deletes or anything get called en route), it displays 0, and indeed if I follow the Cursor I find that it never enters the loop which adds logs to the list which is displayed, also indicating it is not picking up the entry.
I have tried to set the DB insert in a transaction so that I can manually commit, but this does not help either. What makes this more interesting to me is that I have similar functionality elsewhere in my app where I save user preferences and display them in a list, and this does not suffer from the same problem...I have compared against this code and couldn't find any differences that would cause it.
To sum it up, my question is two-fold: why is only my first insert on an empty table showing up as not there, while all following ones are fine?; why am I getting a valid return from the database insert and yet immediately following the insert when I query for that data it is missing?
Thanks in advance for any help you can provide :)
if (cursor.moveToFirst()) {
while (cursor.moveToNext()) {
This skips the first row in cursor. moveToFirst() moves to the first row and moveToNext() moves to the next one, skipping the first one.
You can replace this with just while (cursor.moveToNext()). When you get your cursor from a query, it is placed at index -1 first i.e. at the row before the first one.
if (cursor.moveToFirst()) {
while (cursor.moveToNext()) {
This would be the best solution for it....

Does this if/elseif make my Cursor query the db twice?

Say I've got a Cursor just returned from a database call:
Cursor myCursor = db.rawQuery(someQuery, null);
According to the docs, the Cursor isn't actually populated before any calls are made to it, like getCount() for example. So my question is, does the following code actually query my database twice?
if(myCursor.getCount() > 1)
{
// Do something
}
else if(myCursor.getCount() == 1)
{
// Do something else
}
Or does Android cache the Cursor object after the first 'if' statement, making the 'else if' statement access the cached object instead?
No, it doesn't. The cursor object actually contains it's own count. You can see the source code for the SQLiteCursor object here.Take a look at the getCount() method:
#Override
public int getCount() {
if (mCount == NO_COUNT) {
fillWindow(0);
}
return mCount;
}
It will only check the first time. mCount is only updated when the Cursor is requeried (see QueryThread.run() on the same page).

Android Contentprovider - update within an insert method

Is it ok to call the SQLiteDatabase update method in the insert() overridden method of a content provider?
Basically it's fine, but since you didn't provided code, I just can post 2 possible ways for it
First:
// In your content provider
public Uri insert(...) {
long insertId = db.insert(...);
if(insertId == -1) {
// insert failed, do update
db.update(...);
}
}
Second:
public Uri insert(...) {
long insertId = db.insertWithOnConflict(table, null, values, SQLiteDatabase.CONFLICT_REPLACE)
if(insertId == -1) {
// insert and update/replace failed
}
}
Check out SQLiteDatabase for reference on the forth parameter. In most cases the last method should be sufficient, unless you want only certain fields being updated if the row exists and have all fields if it doesn't.
Most useful need for insertWithOnConflict may be, that you could insert a row and if it already exists, ignore the insert and instead return the Uri/primary key of the already existing row.
It's your choice what you write in your overridden methods.
So yes, it is ok.
I don't know what you're trying to do, but you might want to to take a look on the SQLiteDatabase's replace() method too. Maybe it better suits your needs.

What to do in custom ContentProvider's fillWindow() method?

I'm writing a custom ContentProvider that serves up content consisting of a single, constant string which I represent as a one-row table having columns _id = 0 and value = "SomeString". This string is not stored in a database, so I developed a subclass of CrossProcessCursor that has does everything required to behave like what I described above.
The documentation for CrossProcessCursor is very sparse and doesn't really explain what the fillWindow() method should be doing beyond the obvious. Based on the descriptions of CursorWindow's methods, I put the following together, which I thought should cover it:
public class MyCursor implements CrossProcessCursor {
...
public void fillWindow(int pos, CursorWindow window) {
if (pos != 0) { // There's only one row.
return;
}
window.clear();
window.allocRow(); // TODO: Error check, false = no memory
window.setNumColumns(2);
window.setStartPosition(0);
window.putLong(0, 0, 0);
window.putString("SomeString", 0, 1);
}
}
As expected, it gets called with pos = 0 when a client application requests the content, but the client application throws an exception when it tries to go after the first (and only) row:
Caused by: java.lang.IllegalStateException: UNKNOWN type 48
at android.database.CursorWindow.getLong_native(Native Method)
at android.database.CursorWindow.getLong(CursorWindow.java:380)
at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:108)
at android.database.AbstractCursor.moveToPosition(AbstractCursor.java:194)
at android.database.AbstractCursor.moveToFirst(AbstractCursor.java:248)
at android.database.CursorWrapper.moveToFirst(CursorWrapper.java:86)
...(Snipped)...
Could anyone shed some light on what this method should be doing to return a correct-looking row to the client?
Thanks.
For what you're doing you should check out the MatrixCursor. It uses the AbstractCursor#fillWindow implementation which calls toString on every object. Since you're just sending a string anyway it should work fine for you.

Categories

Resources