What is cursor.setNotificationUri() used for?

2019-01-17 01:19发布

问题:

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?

回答1:

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



回答2:

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.



回答3:

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.