Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android ContentProvider calls bursts of setNotificationUri() to CursorAdapter when many rows are inserted with a batch operation

I have a custom ContentProvider which manages the access to a SQLite database. To load the content of a database table in a ListFragment, I use the LoaderManager with a CursorLoader and a CursorAdapter:

public class MyListFragment extends ListFragment implements LoaderCallbacks<Cursor> {
    // ...
    CursorAdapter mAdapter;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        mAdapter = new CursorAdapter(getActivity(), null, 0);
        setListAdapter(mAdapter);
        getLoaderManager().initLoader(LOADER_ID, null, this);
    }

    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        return new CursorLoader(getActivity(), CONTENT_URI, PROJECTION, null, null, null);
    }

    public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
        mAdapter.swapCursor(c);
    }

    public void onLoaderReset(Loader<Cursor> loader) {
        mAdapter.swapCursor(null);
    }
}

The SQLite database gets updated by a background task which fetches a number of items from a web service and inserts these items into the database through ContentProvider batch operations (ContentResolver#applyBatch()).

Even if this is a batch operation, ContentProvider#insert() gets called for each row is inserted into the database and, in the current implementation, the ContentProvider calls setNotificationUri() for each insert command.

The result is that the CursorAdapter receives bursts of notifications, resulting in the UI being updated too often with consequent annoying flickering effect.

Ideally, when a batch operation is in progress, there should be a way to notify ContentObserver only at the end of any batch operation and not with each insert command.

Does anybody know if this is possible? Please note I can change the ContentProvider implementation and override any of its methods.

like image 238
Lorenzo Polidori Avatar asked Mar 21 '12 08:03

Lorenzo Polidori


2 Answers

I found a simpler solution that I discovered from Google's I/O app. You just have to override the applyBatch method in your ContentProvider and perform all the operations in a transaction. Notifications are not sent until the transaction commits, which results in minimizing the number of ContentProvider change-notifications that are sent out:

@Override
public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {

    final SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
    db.beginTransaction();
    try {
        final int numOperations = operations.size();
        final ContentProviderResult[] results = new ContentProviderResult[numOperations];
        for (int i = 0; i < numOperations; i++) {
            results[i] = operations.get(i).apply(this, results, i);
        }
        db.setTransactionSuccessful();
        return results;
    } finally {
        db.endTransaction();
    }
}
like image 93
Dia Kharrat Avatar answered Oct 18 '22 08:10

Dia Kharrat


To address this exact problem, I overrode applyBatch and set a flag which blocked other methods from sending notifications.

    volatile boolean applyingBatch=false;
    public ContentProviderResult[] applyBatch(
        ArrayList<ContentProviderOperation> operations)
        throws OperationApplicationException {
    applyingBatch=true;
    ContentProviderResult[] result;
    try {
        result = super.applyBatch(operations);
    } catch (OperationApplicationException e) {
        throw e;
    }
    applyingBatch=false;
    synchronized (delayedNotifications) {
        for (Uri uri : delayedNotifications) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
    }
    return result;
}

I exposed a method to "store" notifications to be sent when the batch was complete:

protected void sendNotification(Uri uri) {
    if (applyingBatch) {
        if (delayedNotifications==null) {
            delayedNotifications=new ArrayList<Uri>();
        }
        synchronized (delayedNotifications) {
            if (!delayedNotifications.contains(uri)) {
                delayedNotifications.add(uri);
            }
        }
    } else {
        getContext().getContentResolver().notifyChange(uri, null);
    }
}

And any methods that send notifications employ sendNotification, rather than directly firing a notification.

There may well be better ways of doing this - it certainly seems as though they're ought to be - but that's what I did.

like image 7
Phillip Fitzsimmons Avatar answered Oct 18 '22 07:10

Phillip Fitzsimmons