1/*
2 * Copyright (C) 2009 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.common.content;
18
19import android.content.ContentProvider;
20import android.content.ContentProviderOperation;
21import android.content.ContentProviderResult;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.OperationApplicationException;
25import android.database.sqlite.SQLiteDatabase;
26import android.database.sqlite.SQLiteOpenHelper;
27import android.database.sqlite.SQLiteTransactionListener;
28import android.net.Uri;
29
30import java.util.ArrayList;
31
32/**
33 * General purpose {@link ContentProvider} base class that uses SQLiteDatabase for storage.
34 */
35public abstract class SQLiteContentProvider extends ContentProvider
36        implements SQLiteTransactionListener {
37
38    private static final String TAG = "SQLiteContentProvider";
39
40    private SQLiteOpenHelper mOpenHelper;
41    private volatile boolean mNotifyChange;
42    protected SQLiteDatabase mDb;
43
44    private final ThreadLocal<Boolean> mApplyingBatch = new ThreadLocal<Boolean>();
45    private static final int SLEEP_AFTER_YIELD_DELAY = 4000;
46
47    /**
48     * Maximum number of operations allowed in a batch between yield points.
49     */
50    private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
51
52    /**
53     * @return Number of operations that can be applied at once without a yield point.
54     */
55    public int getMaxOperationsPerYield() {
56        return MAX_OPERATIONS_PER_YIELD_POINT;
57    }
58
59    @Override
60    public boolean onCreate() {
61        Context context = getContext();
62        mOpenHelper = getDatabaseHelper(context);
63        return true;
64    }
65
66    protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
67
68    /**
69     * The equivalent of the {@link #insert} method, but invoked within a transaction.
70     */
71    protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
72
73    /**
74     * The equivalent of the {@link #update} method, but invoked within a transaction.
75     */
76    protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
77            String[] selectionArgs);
78
79    /**
80     * The equivalent of the {@link #delete} method, but invoked within a transaction.
81     */
82    protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
83
84    protected abstract void notifyChange();
85
86    public SQLiteOpenHelper getDatabaseHelper() {
87        return mOpenHelper;
88    }
89
90    private boolean applyingBatch() {
91        return mApplyingBatch.get() != null && mApplyingBatch.get();
92    }
93
94    @Override
95    public Uri insert(Uri uri, ContentValues values) {
96        Uri result = null;
97        boolean applyingBatch = applyingBatch();
98        if (!applyingBatch) {
99            mDb = mOpenHelper.getWritableDatabase();
100            mDb.beginTransactionWithListener(this);
101            try {
102                result = insertInTransaction(uri, values);
103                if (result != null) {
104                    mNotifyChange = true;
105                }
106                mDb.setTransactionSuccessful();
107            } finally {
108                mDb.endTransaction();
109            }
110
111            onEndTransaction();
112        } else {
113            result = insertInTransaction(uri, values);
114            if (result != null) {
115                mNotifyChange = true;
116            }
117        }
118        return result;
119    }
120
121    @Override
122    public int bulkInsert(Uri uri, ContentValues[] values) {
123        int numValues = values.length;
124        mDb = mOpenHelper.getWritableDatabase();
125        mDb.beginTransactionWithListener(this);
126        try {
127            for (int i = 0; i < numValues; i++) {
128                Uri result = insertInTransaction(uri, values[i]);
129                if (result != null) {
130                    mNotifyChange = true;
131                }
132                boolean savedNotifyChange = mNotifyChange;
133                SQLiteDatabase savedDb = mDb;
134                mDb.yieldIfContendedSafely();
135                mDb = savedDb;
136                mNotifyChange = savedNotifyChange;
137            }
138            mDb.setTransactionSuccessful();
139        } finally {
140            mDb.endTransaction();
141        }
142
143        onEndTransaction();
144        return numValues;
145    }
146
147    @Override
148    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
149        int count = 0;
150        boolean applyingBatch = applyingBatch();
151        if (!applyingBatch) {
152            mDb = mOpenHelper.getWritableDatabase();
153            mDb.beginTransactionWithListener(this);
154            try {
155                count = updateInTransaction(uri, values, selection, selectionArgs);
156                if (count > 0) {
157                    mNotifyChange = true;
158                }
159                mDb.setTransactionSuccessful();
160            } finally {
161                mDb.endTransaction();
162            }
163
164            onEndTransaction();
165        } else {
166            count = updateInTransaction(uri, values, selection, selectionArgs);
167            if (count > 0) {
168                mNotifyChange = true;
169            }
170        }
171
172        return count;
173    }
174
175    @Override
176    public int delete(Uri uri, String selection, String[] selectionArgs) {
177        int count = 0;
178        boolean applyingBatch = applyingBatch();
179        if (!applyingBatch) {
180            mDb = mOpenHelper.getWritableDatabase();
181            mDb.beginTransactionWithListener(this);
182            try {
183                count = deleteInTransaction(uri, selection, selectionArgs);
184                if (count > 0) {
185                    mNotifyChange = true;
186                }
187                mDb.setTransactionSuccessful();
188            } finally {
189                mDb.endTransaction();
190            }
191
192            onEndTransaction();
193        } else {
194            count = deleteInTransaction(uri, selection, selectionArgs);
195            if (count > 0) {
196                mNotifyChange = true;
197            }
198        }
199        return count;
200    }
201
202    @Override
203    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
204            throws OperationApplicationException {
205        int ypCount = 0;
206        int opCount = 0;
207        mDb = mOpenHelper.getWritableDatabase();
208        mDb.beginTransactionWithListener(this);
209        try {
210            mApplyingBatch.set(true);
211            final int numOperations = operations.size();
212            final ContentProviderResult[] results = new ContentProviderResult[numOperations];
213            for (int i = 0; i < numOperations; i++) {
214                if (++opCount > getMaxOperationsPerYield()) {
215                    throw new OperationApplicationException(
216                            "Too many content provider operations between yield points. "
217                                    + "The maximum number of operations per yield point is "
218                                    + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
219                }
220                final ContentProviderOperation operation = operations.get(i);
221                if (i > 0 && operation.isYieldAllowed()) {
222                    opCount = 0;
223                    boolean savedNotifyChange = mNotifyChange;
224                    if (mDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY)) {
225                        mDb = mOpenHelper.getWritableDatabase();
226                        mNotifyChange = savedNotifyChange;
227                        ypCount++;
228                    }
229                }
230
231                results[i] = operation.apply(this, results, i);
232            }
233            mDb.setTransactionSuccessful();
234            return results;
235        } finally {
236            mApplyingBatch.set(false);
237            mDb.endTransaction();
238            onEndTransaction();
239        }
240    }
241
242    @Override
243    public void onBegin() {
244        onBeginTransaction();
245    }
246
247    @Override
248    public void onCommit() {
249        beforeTransactionCommit();
250    }
251
252    @Override
253    public void onRollback() {
254        // not used
255    }
256
257    protected void onBeginTransaction() {
258    }
259
260    protected void beforeTransactionCommit() {
261    }
262
263    protected void onEndTransaction() {
264        if (mNotifyChange) {
265            mNotifyChange = false;
266            notifyChange();
267        }
268    }
269}
270