I implemented Content Provider that uses a SQLiteDatabase as its backing data source.
One activity writing to the DB by calling getContentResolver().applyBatch(operations)
, which should be atomic.
protected void onPause() {
new Thread(){
@Override
public void run() {
ArrayList<ContentProviderOperation> ops = new ArrayList<>();
ContentProviderOperation.Builder builder;
for (Tag tag: mTopicAdapter.getTags()) {
builder = ContentProviderOperation.newUpdate(QuizProvider.TAG_URI);
builder.withValue(Tag.Table.SELECTED, tag.getSelectionStatus());
builder.withSelection(Tag.Table._ID + " = " + tag.getId(), null);
ops.add(builder.build());
}
try {
ContentProviderResult[] res = getContentResolver().applyBatch(QuizProvider.AUTHORITY, ops);
Timber.d("Update result: %d", res.length);
getContentResolver().notifyChange(QuizProvider.TAG_URI, null);
getContentResolver().notifyChange(QuizProvider.QUESTION_URI, null);
} catch (RemoteException e) {
e.printStackTrace();
} catch (OperationApplicationException e) {
e.printStackTrace();
}
}
}.start();
super.onPause();
}
Second activity reading from the DB with the help of Cursor Loader, and sometimes gets old data (race condition).
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri randQuestionUri = QuizProvider.QUESTION_URI
.buildUpon()
.appendPath("rand").appendPath(Integer.toString(QUIZ_SIZE))
.build();
return new CursorLoader(this, randQuestionUri, null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if (DEBUG) Timber.d("load finished: %d", data.hashCode());
mPagerAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mPagerAdapter.swapCursor(null);
}
Full project here.
Log output:
NSA:QuizProvider:138: update db: selected=false _id = 4
NSA:QuizProvider:138: update db: selected=false _id = 5
NSA:QuizProvider:138: update db: selected=false _id = 26
NSA:QuizProvider:138: update db: selected=false _id = 19
NSA:QuizProvider:138: update db: selected=false _id = 28
NSA:QuizProvider:138: update db: selected=false _id = 10
NSA:QuizProvider:138: update db: selected=false _id = 12
NSA:QuizProvider:138: update db: selected=false _id = 15
NSA:QuizProvider:138: update db: selected=false _id = 18
NSA:QuizProvider:138: update db: selected=false _id = 25
NSA:QuizProvider:138: update db: selected=false _id = 16
NSA:QuizProvider:138: update db: selected=false _id = 17
NSA:QuizProvider:138: update db: selected=false _id = 8
NSA:QuizProvider:138: update db: selected=false _id = 3
NSA:QuizProvider:138: update db: selected=false _id = 20
NSA:QuizProvider:138: update db: selected=false _id = 29
NSA:QuizProvider:138: update db: selected=false _id = 24
NSA:QuizProvider:138: update db: selected=false _id = 23
NSA:QuizProvider:138: update db: selected=false _id = 30
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuizProvider:103: query db: content://doit.study.droi question/ran 280 null null
NSA:QuizProvider:138: update db: selected=false _id = 6
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuizProvider:138: update db: selected=false _id = 1
NSA:QuizProvider:138: update db: selected=false _id = 14
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuizProvider:138: update db: selected=false _id = 7
NSA:QuizProvider:138: update db: selected=false _id = 27
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuestionsActivity:84: load finished: 154982045
NSA:QuestionsPagerAdapter:66: swap cursor, cnt: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:35: instantiateItem, pos=0
NSA:QuestionsPagerAdapter:25: getItem, pos=0
NSA:QuestionsPagerAdapter:35: instantiateItem, pos=1
NSA:QuestionsPagerAdapter:25: getItem, pos=1
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuizProvider:138: update db: selected=false _id = 2
NSA:QuizProvider:138: update db: selected=false _id = 11
NSA:QuizProvider:138: update db: selected=false _id = 22
NSA:QuizProvider:138: update db: selected=false _id = 9
NSA:QuizProvider:138: update db: selected=false _id = 31
NSA:QuizProvider:138: update db: selected=false _id = 21
NSA:QuizProvider:138: update db: selected=false _id = 32
NSA:QuizProvider:138: update db: selected=false _id = 13
NSA:QuizProvider:138: update db: selected=false _id = 4
W/FragmentManager: moveToState: Fragment state for QuestionFragment{5c25e01 #0 id=0x7f0f00e5} not updated inline; expected state 3 found 2
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:52: title pos: 0, questions: tags: [User Interfaces]
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:52: title pos: 1, questions: tags: [User Interfaces]
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuizProvider:103: query db: content://doit.study.droi tag null null
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuestionsPagerAdapter:42: counter: 104
NSA:QuizProvider:138: update db: selected=false _id = 5
NSA:QuizProvider:138: update db: selected=false _id = 26
NSA:QuizProvider:103: query db: content://doit.study.droi tag null null
NSA:QuizProvider:138: update db: selected=false _id = 19
NSA:QuizProvider:138: update db: selected=false _id = 28
NSA:QuizProvider:138: update db: selected=false _id = 10
NSA:QuizProvider:138: update db: selected=false _id = 12
NSA:QuizProvider:138: update db: selected=false _id = 15
NSA:QuizProvider:138: update db: selected=false _id = 18
NSA:QuizProvider:138: update db: selected=false _id = 25
NSA:QuizProvider:138: update db: selected=false _id = 16
NSA:QuizProvider:138: update db: selected=false _id = 17
NSA:QuizProvider:138: update db: selected=false _id = 8
NSA:QuizProvider:138: update db: selected=false _id = 3
NSA:QuizProvider:138: update db: selected=false _id = 20
NSA:QuizProvider:138: update db: selected=false _id = 29
NSA:QuizProvider:138: update db: selected=false _id = 24
NSA:QuizProvider:138: update db: selected=false _id = 23
NSA:QuizProvider:138: update db: selected=false _id = 30
NSA:QuizProvider:138: update db: selected=false _id = 6
NSA:QuizProvider:138: update db: selected=false _id = 1
NSA:QuizProvider:138: update db: selected=false _id = 14
NSA:QuizProvider:138: update db: selected=false _id = 7
NSA:QuizProvider:138: update db: selected=false _id = 27
NSA:QuizProvider:138: update db: selected=false _id = 2
NSA:QuizProvider:138: update db: selected=false _id = 11
NSA:QuizProvider:138: update db: selected=false _id = 22
NSA:QuizProvider:138: update db: selected=false _id = 9
NSA:QuizProvider:138: update db: selected=false _id = 31
NSA:QuizProvider:138: update db: selected=false _id = 21
NSA:QuizProvider:138: update db: selected=false _id = 32
NSA:QuizProvider:138: update db: selected=false _id = 13
NSA:QuizProvider:103: query db: content://doit.study.droi question/ran 280 null null
NSA:QuestionsActivity:84: load finished: 19434496
NSA:QuestionsPagerAdapter:66: swap cursor, cnt: 0
NSA:QuestionsPagerAdapter:42: counter: 0
NSA:QuestionsPagerAdapter:42: counter: 0
Log shows that applyBatch is not finished and Cursor Loader gets partially modified data (cursor counter=104, should be 0 or 280).
Some resources (sorry, cannot add more then two links):
_http://developer.android.com/guide/topics/providers/content-provider-basics.html#Batch
_http://www.androiddesignpatterns.com/2012/10/sqlite-contentprovider-thread-safety.html
_http://stackoverflow.com/questions/8104832/sqlite-simultaneous-reading-and-writing
_http://www.grokkingandroid.com/better-performance-with-contentprovideroperation/
Do you have any ideas what's wrong?
There are basically three things to consider here:
notifyChange()
calls to your ContentProvider. That way you can be sure that they are called whenever you make a change. Or in other words: You might forget to do so somewhere else. The client shouldn't be responsible for this - it doesn't belong here.applyBatch()
method actually uses transactions. only then do you get the desired performance benefits and only then locks are used in the way you need.applyBatch()
is running, no notifications are emitted. Otherwise your Loader would be called too often. You definitely want to avoid that.You can see my cpsample project for a content provider that follows these rules.