1/*
2 * Copyright (C) 2011 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.providers.contacts;
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.SQLiteOpenHelper;
26import android.database.sqlite.SQLiteTransactionListener;
27import android.net.Uri;
28import android.util.Log;
29
30import java.util.ArrayList;
31
32/**
33 * A common base class for the contacts and profile providers.  This handles much of the same
34 * logic that SQLiteContentProvider does (i.e. starting transactions on the appropriate database),
35 * but exposes awareness of batch operations to the subclass so that cross-database operations
36 * can be supported.
37 */
38public abstract class AbstractContactsProvider extends ContentProvider
39        implements SQLiteTransactionListener {
40
41    public static final String TAG = "ContactsProvider";
42
43    public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
44
45    /** Set true to enable detailed transaction logging. */
46    public static final boolean ENABLE_TRANSACTION_LOG = false; // Don't submit with true.
47
48    /**
49     * Duration in ms to sleep after successfully yielding the lock during a batch operation.
50     */
51    protected static final int SLEEP_AFTER_YIELD_DELAY = 4000;
52
53    /**
54     * Maximum number of operations allowed in a batch between yield points.
55     */
56    private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
57
58    /**
59     * Number of inserts performed in bulk to allow before yielding the transaction.
60     */
61    private static final int BULK_INSERTS_PER_YIELD_POINT = 50;
62
63    /**
64     * The contacts transaction that is active in this thread.
65     */
66    private ThreadLocal<ContactsTransaction> mTransactionHolder;
67
68    /**
69     * The DB helper to use for this content provider.
70     */
71    private SQLiteOpenHelper mDbHelper;
72
73    /**
74     * The database helper to serialize all transactions on.  If non-null, any new transaction
75     * created by this provider will automatically retrieve a writable database from this helper
76     * and initiate a transaction on that database.  This should be used to ensure that operations
77     * across multiple databases are all blocked on a single DB lock (to prevent deadlock cases).
78     *
79     * Hint: It's always {@link ContactsDatabaseHelper}.
80     *
81     * TODO Change the structure to make it obvious that it's actually always set, and is the
82     * {@link ContactsDatabaseHelper}.
83     */
84    private SQLiteOpenHelper mSerializeOnDbHelper;
85
86    /**
87     * The tag corresponding to the database used for serializing transactions.
88     *
89     * Hint: It's always the contacts db helper tag.
90     *
91     * See also the TODO on {@link #mSerializeOnDbHelper}.
92     */
93    private String mSerializeDbTag;
94
95    /**
96     * The transaction listener used with {@link #mSerializeOnDbHelper}.
97     *
98     * Hint: It's always {@link ContactsProvider2}.
99     *
100     * See also the TODO on {@link #mSerializeOnDbHelper}.
101     */
102    private SQLiteTransactionListener mSerializedDbTransactionListener;
103
104    @Override
105    public boolean onCreate() {
106        Context context = getContext();
107        mDbHelper = getDatabaseHelper(context);
108        mTransactionHolder = getTransactionHolder();
109        return true;
110    }
111
112    public SQLiteOpenHelper getDatabaseHelper() {
113        return mDbHelper;
114    }
115
116    /**
117     * Specifies a database helper (and corresponding tag) to serialize all transactions on.
118     *
119     * See also the TODO on {@link #mSerializeOnDbHelper}.
120     */
121    public void setDbHelperToSerializeOn(SQLiteOpenHelper serializeOnDbHelper, String tag,
122            SQLiteTransactionListener listener) {
123        mSerializeOnDbHelper = serializeOnDbHelper;
124        mSerializeDbTag = tag;
125        mSerializedDbTransactionListener = listener;
126    }
127
128    public ContactsTransaction getCurrentTransaction() {
129        return mTransactionHolder.get();
130    }
131
132    @Override
133    public Uri insert(Uri uri, ContentValues values) {
134        ContactsTransaction transaction = startTransaction(false);
135        try {
136            Uri result = insertInTransaction(uri, values);
137            if (result != null) {
138                transaction.markDirty();
139            }
140            transaction.markSuccessful(false);
141            return result;
142        } finally {
143            endTransaction(false);
144        }
145    }
146
147    @Override
148    public int delete(Uri uri, String selection, String[] selectionArgs) {
149        ContactsTransaction transaction = startTransaction(false);
150        try {
151            int deleted = deleteInTransaction(uri, selection, selectionArgs);
152            if (deleted > 0) {
153                transaction.markDirty();
154            }
155            transaction.markSuccessful(false);
156            return deleted;
157        } finally {
158            endTransaction(false);
159        }
160    }
161
162    @Override
163    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
164        ContactsTransaction transaction = startTransaction(false);
165        try {
166            int updated = updateInTransaction(uri, values, selection, selectionArgs);
167            if (updated > 0) {
168                transaction.markDirty();
169            }
170            transaction.markSuccessful(false);
171            return updated;
172        } finally {
173            endTransaction(false);
174        }
175    }
176
177    @Override
178    public int bulkInsert(Uri uri, ContentValues[] values) {
179        ContactsTransaction transaction = startTransaction(true);
180        int numValues = values.length;
181        int opCount = 0;
182        try {
183            for (int i = 0; i < numValues; i++) {
184                insert(uri, values[i]);
185                if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) {
186                    opCount = 0;
187                    try {
188                        yield(transaction);
189                    } catch (RuntimeException re) {
190                        transaction.markYieldFailed();
191                        throw re;
192                    }
193                }
194            }
195            transaction.markSuccessful(true);
196        } finally {
197            endTransaction(true);
198        }
199        return numValues;
200    }
201
202    @Override
203    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
204            throws OperationApplicationException {
205        if (VERBOSE_LOGGING) {
206            Log.v(TAG, "applyBatch: " + operations.size() + " ops");
207        }
208        int ypCount = 0;
209        int opCount = 0;
210        ContactsTransaction transaction = startTransaction(true);
211        try {
212            final int numOperations = operations.size();
213            final ContentProviderResult[] results = new ContentProviderResult[numOperations];
214            for (int i = 0; i < numOperations; i++) {
215                if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
216                    throw new OperationApplicationException(
217                            "Too many content provider operations between yield points. "
218                                    + "The maximum number of operations per yield point is "
219                                    + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
220                }
221                final ContentProviderOperation operation = operations.get(i);
222                if (i > 0 && operation.isYieldAllowed()) {
223                    if (VERBOSE_LOGGING) {
224                        Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield...");
225                    }
226                    opCount = 0;
227                    try {
228                        if (yield(transaction)) {
229                            ypCount++;
230                        }
231                    } catch (RuntimeException re) {
232                        transaction.markYieldFailed();
233                        throw re;
234                    }
235                }
236
237                results[i] = operation.apply(this, results, i);
238            }
239            transaction.markSuccessful(true);
240            return results;
241        } finally {
242            endTransaction(true);
243        }
244    }
245
246    /**
247     * If we are not yet already in a transaction, this starts one (on the DB to serialize on, if
248     * present) and sets the thread-local transaction variable for tracking.  If we are already in
249     * a transaction, this returns that transaction, and the batch parameter is ignored.
250     * @param callerIsBatch Whether the caller is operating in batch mode.
251     */
252    private ContactsTransaction startTransaction(boolean callerIsBatch) {
253        if (ENABLE_TRANSACTION_LOG) {
254            Log.i(TAG, "startTransaction " + getClass().getSimpleName() +
255                    "  callerIsBatch=" + callerIsBatch, new RuntimeException("startTransaction"));
256        }
257        ContactsTransaction transaction = mTransactionHolder.get();
258        if (transaction == null) {
259            transaction = new ContactsTransaction(callerIsBatch);
260            if (mSerializeOnDbHelper != null) {
261                transaction.startTransactionForDb(mSerializeOnDbHelper.getWritableDatabase(),
262                        mSerializeDbTag, mSerializedDbTransactionListener);
263            }
264            mTransactionHolder.set(transaction);
265        }
266        return transaction;
267    }
268
269    /**
270     * Ends the current transaction and clears out the member variable.  This does not set the
271     * transaction as being successful.
272     * @param callerIsBatch Whether the caller is operating in batch mode.
273     */
274    private void endTransaction(boolean callerIsBatch) {
275        if (ENABLE_TRANSACTION_LOG) {
276            Log.i(TAG, "endTransaction " + getClass().getSimpleName() +
277                    "  callerIsBatch=" + callerIsBatch, new RuntimeException("endTransaction"));
278        }
279        ContactsTransaction transaction = mTransactionHolder.get();
280        if (transaction != null && (!transaction.isBatch() || callerIsBatch)) {
281            try {
282                if (transaction.isDirty()) {
283                    notifyChange();
284                }
285                transaction.finish(callerIsBatch);
286            } finally {
287                // No matter what, make sure we clear out the thread-local transaction reference.
288                mTransactionHolder.set(null);
289            }
290        }
291    }
292
293    /**
294     * Gets the database helper for this contacts provider.  This is called once, during onCreate().
295     */
296    protected abstract SQLiteOpenHelper getDatabaseHelper(Context context);
297
298    /**
299     * Gets the thread-local transaction holder to use for keeping track of the transaction.  This
300     * is called once, in onCreate().  If multiple classes are inheriting from this class that need
301     * to be kept in sync on the same transaction, they must all return the same thread-local.
302     */
303    protected abstract ThreadLocal<ContactsTransaction> getTransactionHolder();
304
305    protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
306
307    protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
308
309    protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
310            String[] selectionArgs);
311
312    protected abstract boolean yield(ContactsTransaction transaction);
313
314    protected abstract void notifyChange();
315}
316