0

Why might Cursor.getLong() and Cursor.getString() work correctly on an emulator but not an actual device running the same version of Android?

I made some additions to version three of Android's Notepad tutorial that involve programmatically changing notes' titles. (I recently asked about this project at Obtaining the current Android view and forcing it to be redrawn.) My changes work fine in my emulator, but cause an app-ending "Unfortunately, program has stopped" on my testing phone.

Here's the method where this is happening:

public void expandNoteTitle(int i) {
    Cursor note = fetchNote(i);
    long rowId =
      note.getLong(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_ROWID));
    String title =
      note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)) + "W";
    String body =
      note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY));
    updateNote(rowId, title, body);
}

I know that fetching the cursor is at least partially working because note.getColumnName(1) works fine in the debugger. I also confirmed that the arguments to getLong() and getString() show up correctly.

Here's the LogCat output:

08-12 15:35:29.735: D/AndroidRuntime(17937): Shutting down VM
08-12 15:35:29.735: W/dalvikvm(17937): threadid=1: thread exiting with uncaught exception (group=0x40a3d1f8)
08-12 15:35:29.751: E/AndroidRuntime(17937): FATAL EXCEPTION: main
08-12 15:35:29.751: E/AndroidRuntime(17937): android.database.CursorIndexOutOfBoundsException: Index 0 requested, with a size of 0
08-12 15:35:29.751: E/AndroidRuntime(17937):    at android.database.AbstractCursor.checkPosition(AbstractCursor.java:400)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at android.database.AbstractWindowedCursor.checkPosition(AbstractWindowedCursor.java:136)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at android.database.AbstractWindowedCursor.getLong(AbstractWindowedCursor.java:74)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at notepad.NotesDbAdapter.expandNoteTitle(NotesDbAdapter.java:212)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at notepad.NotesDbAdapter.expandNoteTitles(NotesDbAdapter.java:201)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at notepad.NotepadMain.expandTitles(NotepadMain.java:167)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at notepad.NotepadMain.onMenuItemSelected(NotepadMain.java:115)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at com.android.internal.policy.impl.PhoneWindow.onMenuItemSelected(PhoneWindow.java:950)
08-12 15:35:29.751: E/AndroidRuntime(17937):    at [SNIPPED FOR LENGTH]
08-12 15:35:29.751: E/AndroidRuntime(17937):    at dalvik.system.NativeStart.main(Native Method)
08-12 15:36:49.993: I/Process(17937): Sending signal. PID: 17937 SIG: 9

And here's the code for fetchNote(). It is the same as the version given in the tutorial except for a variable name.

public Cursor fetchNote(long rowId) throws SQLException {
    Cursor mCursor = database.query(true, DATABASE_TABLE, new String[] {
      KEY_ROWID, KEY_TITLE, KEY_BODY }, KEY_ROWID + "=" + rowId, null,
      null, null, null, null);
    if(mCursor != null) {
        mCursor.moveToFirst();
    }
    return mCursor;
}
Community
  • 1
  • 1
Pops
  • 30,199
  • 37
  • 136
  • 151

2 Answers2

1

Cursor's getLong() and getString() work on an emulator but not an equivalent device... I know that fetching the cursor is at least partially working because note.getColumnName(1) works fine in the debugger. I also confirmed that the arguments to getLong() and getString() show up correctly.

Cursor's getLong() and getString() methods work identically on both emulators and actual devices.

If you look at your LogCat, you'll see the cause of your problem is that there is no data:

android.database.CursorIndexOutOfBoundsException: Index 0 requested, with a size of 0

It appears that the Cursor is empty, does your table (on the device) have any rows?


Added from comments

As we discovered you made the assumption that all of your row id's were sequential, but that is not true. Here are my two suggestions to fetch the right data and prevent any future force closes from your Cursors:

  1. Fetch all of the existing row ids and iterate through them:

    Cursor notes = database.query(DATABASE_TABLE, new String[] { KEY_ROWID }, null, null, null, null, null, null);
    
    while(notes.moveToNext()) 
        expandNoteTitle(notes.getLong(0)); 
    

    To iterate through a new Cursor you only to check the return value from moveToNext(). When the Cursor's index is set to -1 (like when it's new), moveToNext() will move to index 0 and return true only if there is valid data, otherwise if the are no rows moveToNext() returns false.

  2. Always assume that a Cursor could be empty and understand that an empty cursor does not equal null:

    public Cursor fetchNote(long rowId) throws SQLException {
        return database.query(true, DATABASE_TABLE, 
                new String[] { KEY_ROWID, KEY_TITLE, KEY_BODY }, 
                KEY_ROWID + "=" + rowId, null, null, null, null, null);
    }
    
    public void expandNoteTitle(long rowId) {
        Cursor note = fetchNote(rowId);
        if(note.getCount() > 0) {
            // You could also use if(cursor.moveToNext()) or if(cursor.moveToFirst()), both of these return true / false depending on whether there is valid data
    
            String title =
              note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)) + "W";
            String body =
              note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY));
            updateNote(rowId, title, body);
        }
    }
    
Sam
  • 86,580
  • 20
  • 181
  • 179
  • Yes, there are eight saved notes (rows). All of them show up normally when the app loads, before I try to edit them. – Pops Aug 12 '12 at 19:57
  • I've added it to the question. It's exactly the same as the code given in [the Android Developers tutorial](http://developer.android.com/training/notepad/notepad-ex3.html) except for a changed variable name. – Pops Aug 12 '12 at 20:03
  • Ok, it looks fine. So the id (`int i`) doesn't refer to an actual row. Where does this id come from? – Sam Aug 12 '12 at 20:06
  • I'm asking the database for a list of all its rows, getting its size and then iterating through each one. – Pops Aug 12 '12 at 20:08
  • 1
    Your comment gave me a new idea. I don't think I ever deleted any rows from the table in the emulator. I just did that, and now it's crashing, too. I'm now thinking the ID is the primary key from the database, and I can't just step through from ID 1 to ID number-of-rows. – Pops Aug 12 '12 at 20:23
  • Correct. You should do two things. 1) fetch the valid ids with `SELECT _id FROM Table` and iterate through this Cursor calling getLong() for each valid id. 2) In expandNoteTitle() (or anywhere you use Cursor's getLong(), getString(), etc) check `if(cursor.getCount() > 0)`. You **cannot** use `if(cursor == null)` because a empty Cursor does not equal null and calling getLong() will throw an Out of Bounds exception. – Sam Aug 12 '12 at 21:36
  • @LordTorgamus Glad I could help! (I deleted my duplicate comments from above... apparently my answer was deemed "not an answer" despite helping you find a solution. I "appealed" and obviously it is an answer again.) – Sam Aug 13 '12 at 20:45
  • Ah, good, I'll clean up my comments there as well. I was actually the one who flagged you. The initial version of your answer was really more of a comment/request to start a discussion. But I agree that the current version is good (obviously). Thanks again! – Pops Aug 13 '12 at 20:51
0

The discrepancy here was caused by coincidence. At the time I asked this question, I had never deleted a note in the emulator. As a result, the notes I had there had database IDs of 1, 2, 3 and 4. When I asked the database to return the number of notes it contained, I set up a

for(int i = 1; i <= numberOfNotes; i++) { ... }

loop. That matched the existing IDs only because the IDs are assigned with the next highest integer upon note creation. On the device, I had deleted a few notes, so the IDs weren't a nice 1-to-n sequence. Here's what the code should look like:

public void expandNoteTitles() {
    Cursor notes = database.query(DATABASE_TABLE, new String[] { KEY_ROWID },
      null, null, null, null, null, null);
    notes.moveToFirst();

    for(int i = 0; i < notes.getCount(); i++) {
        expandNoteTitle(notes.getLong(0));
        notes.moveToNext();
    }
}

public void expandNoteTitle(long rowId) {
    Cursor note = fetchNote(rowId);
    String title =
      note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_TITLE)) + "W";
    String body =
      note.getString(note.getColumnIndexOrThrow(NotesDbAdapter.KEY_BODY));
    updateNote(rowId, title, body);
}
Pops
  • 30,199
  • 37
  • 136
  • 151