11

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.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
Lorenzo Polidori
  • 10,332
  • 10
  • 51
  • 60
  • 4
    Consider doing / basing your provider on something like [SQLiteContentProvider](http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android-apps/2.2_r1.1/com/android/providers/calendar/SQLiteContentProvider.java/?v=source) - it provides much of the underpinnings of a nice and functional provider based on SQLite - and if you do this - you should simply use bulkInsert or applyBatch to do your "mass inserts". The added benefit is that your bulk inserts will be performed in a transaction which speeds them up quite a lot. – Jens Mar 21 '12 at 10:16
  • @Jens many thanks for the pointer, this looks like exactly what I was after. Just a question about that: who has developed and released this code? From the header, it seems to be part of the The Android Open Source Project, but, if this is the case, why has it not been released by Google with the standard Android SDK? – Lorenzo Polidori Mar 22 '12 at 09:03
  • 2
    It's part of the AOSP project but not published in the SDK - why I don't know since including it would've prevented a lot of people from writing crappy `ContentProvider` implementations of their own. – Jens Mar 22 '12 at 11:21
  • I just added a feature request issue regarding this: http://code.google.com/p/android/issues/detail?id=28597 – Macarse Apr 11 '12 at 01:58
  • Why don't @Jens move his comment to answer and Lorenzo Polidori accepts it as an answer. It will good for the community. – Gaurav Agarwal May 21 '12 at 21:28

3 Answers3

13

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();
    }
}
Dia Kharrat
  • 5,948
  • 3
  • 32
  • 43
  • Thanks @Dia, it seems the best and simplest solution. And since it's too simple and generic enough, I wonder why google don't make this as default for all batch operations – xandy Jul 15 '13 at 04:54
  • For me this fires off just as many events because its still calling the insert(...) method multiple times and hence calling its notifyChange(uri, null) that is in the insert(...) method. – startoftext Aug 13 '14 at 20:06
  • Just one comment, "which results in minimizing the number of" in fact it just pospones the notification to the end of the execution. Thanks for the proposed solution! – Juan Saravia Nov 04 '14 at 19:05
  • 2
    You could replace that `for` loop with `ContentProviderResult[] results = super.applyBatch(operations);`. – ooi Jan 04 '15 at 16:39
7

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.

Phillip Fitzsimmons
  • 2,915
  • 2
  • 21
  • 20
  • 1
    Many thanks for your answer. +1 for the idea, which is one of the options I had in mind, but I would rather use the more flexible and generic approach of deriving from the abstract [`SQLiteContentProvider`](http://grepcode.com/file_/repository.grepcode.com/java/ext/com.google.android/android-apps/2.2_r1.1/com/android/providers/calendar/SQLiteContentProvider.java/?v=source) suggested by Jens. – Lorenzo Polidori Mar 22 '12 at 09:11
0

In a comment to the original answer, Jens directed us towards SQLiteContentProvider in the AOSP. One reason why this isn't (yet?) in the SDK may be that the AOSP seems to contain multiple variations of this code.

For example com.android.browser.provider.SQLiteContentProvider seems to be a slightly more complete solution, incorporating the "delayed notifications" principle proposed by Phillip Fitzsimmons while keeping the provider thread-safe by using a ThreadLocal for the batch-flag and synchronizing access to the delayed notification set.

Yet even though access to the set of URI's to be notified of change is synchronized, I can still imagine that race conditions may occur. For example if a long operation posts some notifications, then gets overtaken by a smaller batch operation, which fires the notifications and clears the set before the first operation commits.

Still, the above version seems to be the best bet as a reference when implementing your own provider.