Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

registerContentObserver() on raw SQLite Cursor

All of the examples I've seen of using registerContentObserver() do so through a ContentProvider interface. But Cursor has a registerContentObserver() call, so I figured maybe the Android folks have put together some deep magic that would allow for getting updates on a SQLite cursor when one of the rows from an active result set changed. Either I'm doing it wrong, or there is no such magic. Here's the code I'm working with, the expectation being that when I click on the button to update the value in row #1 I would get an onChange() callback. Any ideas?

public class MainActivity extends Activity {
    private static final String DATABASE_NAME = "test.db";
    public static final String TAG = "dbtest";

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Button make = (Button)findViewById(R.id.btn_make_record);
        make.setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                MyOpenHelper oh = new MyOpenHelper(v.getContext());
                SQLiteDatabase wdb = oh.getWritableDatabase();
                ContentValues cv = new ContentValues();
                cv.put("value", String.valueOf(System.currentTimeMillis()));
                if (wdb.insert(MyOpenHelper.TABLE_NAME, null, cv) == -1) {
                    Log.d(TAG, "Unable to insert row");
                } else {
                    Log.d(TAG, "Inserted row ");
                }
                wdb.close();
            }
        });

        Button update = (Button)findViewById(R.id.btn_update_record);
        update.setOnClickListener(new OnClickListener() {   
            public void onClick(View v) {
                MyOpenHelper oh = new MyOpenHelper(v.getContext());
                SQLiteDatabase wdb = oh.getWritableDatabase();
                ContentValues cv = new ContentValues();
                cv.put("value", String.valueOf(System.currentTimeMillis()));
                int count = wdb.update(MyOpenHelper.TABLE_NAME, cv, "_id = ?", new String[] {"1"});
                Log.d(TAG, "Updated " + count + " row(s)");
                wdb.close();
            }
        });

        MyOpenHelper oh = new MyOpenHelper(this);
        SQLiteDatabase rdb = oh.getReadableDatabase();
        Cursor c = rdb.query(MyOpenHelper.TABLE_NAME, null, "_id = ?", new String[] {"1"}, null, null, null);
        startManagingCursor(c);
        contentObserver = new MyContentObserver(new Handler());
        c.registerContentObserver(contentObserver);
    }

    private class MyContentObserver extends ContentObserver {
        MyContentObserver(Handler handler) {
            super(handler);
        }

        public boolean deliverSelfNotifications() {
            return true;
        }

        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            Log.d(TAG, "Saw a change in row # 1");
        }
    }

    MyContentObserver contentObserver;

    public class MyOpenHelper extends SQLiteOpenHelper {
        private static final int DATABASE_VERSION = 1;
        private static final String TABLE_NAME = "test";
        private static final String TABLE_CREATE =
                "CREATE TABLE " + TABLE_NAME + " (" +
                "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
                "value TEXT);";

        MyOpenHelper(Context context) {
             super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(TABLE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
          // TODO Auto-generated method stub    
        }
    }
}
like image 802
mikerowehl Avatar asked Jun 24 '11 09:06

mikerowehl


People also ask

Does SQLite support cursors?

The sqlite3. Cursor class is an instance using which you can invoke methods that execute SQLite statements, fetch data from the result sets of the queries. You can create Cursor object using the cursor() method of the Connection object/class.

What does cursor moveToFirst do?

"A Cursor object, which is positioned before the first entry." Calling moveToFirst() does two things: it allows you to test whether the query returned an empty set (by testing the return value) and it moves the cursor to the first result (when the set is not empty).

What is cursor getColumnIndex?

getColumnIndex(String columnName) Returns the zero-based index for the given column name, or -1 if the column doesn't exist. abstract int. getColumnIndexOrThrow(String columnName) Returns the zero-based index for the given column name, or throws IllegalArgumentException if the column doesn't exist.

Which method is used to get data from cursor?

Once the cursor's position is pointing to a valid row, the columns of the row can be read from the cursor. To read the data, the code in Listing 5.10 uses two methods from the cursor class: Cursor. getColumnIndexOrThrow() and one of the type get() methods from the Cursor class.


2 Answers

This is actually possible.

You have to define an Uri somewhere (a constant maybe).

I would advice to use something like this:

public static final Uri URI_MY_TABLE = 
    Uri.parse("sqlite://com.example.<your-app-package>/table");

Then when you update the database data you call:

context.getContentResolver().notifyChange(Constants.URI_MY_TABLE, null);

And from the CursorLoader you are writing do something like:

final ForceLoadContentObserver observer;

public MyCursorLoader(final Context context, ...) {
    super(context);
    // ...
    this.observer = new ForceLoadContentObserver();
}

@Override
public Cursor loadInBackground() {
    SQLiteDatabase db = this.dbHelper.getReadableDatabase();
    final Cursor c = queryDatabase(db);
    if (c != null) {
        // Ensure the cursor window is filled
        c.getCount();
        // this is to force a reload when the content change
        c.registerContentObserver(this.observer);
        // this make sure this loader will be notified when
        // a notifyChange is called on the URI_MY_TABLE
        c.setNotificationUri(getContext().getContentResolver(),
            Constants.URI_MY_TABLE);
    }
    return c;
}

The ForceLoadContentObserver is a public static inner class inside the Loader class, if you are using the Support Library it will be android.support.v4.content.Loader.ForceLoadContentObserver

In the same Loader make sure you forceReload if data change:

@Override
protected void onStartLoading() {
    if (this.cursor != null) {
        deliverResult(this.cursor);
    }
    // this takeContentChanged() is important!
    if (takeContentChanged() || this.cursor == null) {
        forceLoad();
    }
}

A good start for your CursorLoader if you do not know how to write it is this: CursorLoader usage without ContentProvider

Your CursorAdapter should now be initialized like this:

public MyCursorAdapter(Context context, Cursor c) {
    super(context, c, 0);
}

The ContentResolver will take care of notifying observer on the uri you set up.

This is exactly how the ContentProvider works.

If you are not using a Loader you can do the same stuff, the important modifications are:

  • Call ContentResolver.notifyChange(URI,null); when you change the data
  • Call Cursor.setNotificationUri(ContentResolver, URI); when you load the cursor

This way when you register an observer you will be notified when the underling data change.

like image 135
Daniele Segato Avatar answered Oct 27 '22 04:10

Daniele Segato


No takers huh? Well an excellent excuse to dig in myself and figure it out. For those who might be curious after me, if you download the platform source the relevant files to look at are:

frameworks/base/core/java/android/database/sqlite/SQLiteCursor.java frameworks/base/core/java/android/database/AbstractCursor.java

It looks like the mContentObservable is there to signal different parts of a program running off the cached result set in a Cursor, and the observable is a signal from the cached result set to let consumers know when the cache has been dumped/updated. It doesn't tie into the SQLite implementation to fire events when the underlying datastore is manipulated.

Not exactly surprising, but given the docs I thought maybe I was missing something.

like image 32
mikerowehl Avatar answered Oct 27 '22 03:10

mikerowehl