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 
17 package com.android.providers.contacts;
18 
19 import android.content.ContentProvider;
20 import android.content.ContentProviderOperation;
21 import android.content.ContentProviderResult;
22 import android.content.ContentValues;
23 import android.content.Context;
24 import android.content.OperationApplicationException;
25 import android.database.Cursor;
26 import android.database.DatabaseUtils;
27 import android.database.sqlite.SQLiteDatabase;
28 import android.database.sqlite.SQLiteOpenHelper;
29 import android.database.sqlite.SQLiteTransactionListener;
30 import android.net.Uri;
31 import android.os.Binder;
32 import android.os.SystemClock;
33 import android.provider.BaseColumns;
34 import android.provider.ContactsContract.Data;
35 import android.provider.ContactsContract.RawContacts;
36 import android.util.Log;
37 
38 import com.android.internal.util.ProviderAccessStats;
39 import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
40 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
41 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
42 
43 import java.io.PrintWriter;
44 import java.util.ArrayList;
45 
46 /**
47  * A common base class for the contacts and profile providers.  This handles much of the same
48  * logic that SQLiteContentProvider does (i.e. starting transactions on the appropriate database),
49  * but exposes awareness of batch operations to the subclass so that cross-database operations
50  * can be supported.
51  */
52 public abstract class AbstractContactsProvider extends ContentProvider
53         implements SQLiteTransactionListener {
54 
55     public static final String TAG = "ContactsProvider";
56 
57     public static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
58 
59     /** Set true to enable detailed transaction logging. */
60     public static final boolean ENABLE_TRANSACTION_LOG = false; // Don't submit with true.
61 
62     /**
63      * Duration in ms to sleep after successfully yielding the lock during a batch operation.
64      */
65     protected static final int SLEEP_AFTER_YIELD_DELAY = 4000;
66 
67     /**
68      * Maximum number of operations allowed in a batch between yield points.
69      */
70     private static final int MAX_OPERATIONS_PER_YIELD_POINT = 500;
71 
72     /**
73      * Number of inserts performed in bulk to allow before yielding the transaction.
74      */
75     private static final int BULK_INSERTS_PER_YIELD_POINT = 50;
76 
77     /**
78      * The contacts transaction that is active in this thread.
79      */
80     private ThreadLocal<ContactsTransaction> mTransactionHolder;
81 
82     /**
83      * The DB helper to use for this content provider.
84      */
85     private ContactsDatabaseHelper mDbHelper;
86 
87     /**
88      * The database helper to serialize all transactions on.  If non-null, any new transaction
89      * created by this provider will automatically retrieve a writable database from this helper
90      * and initiate a transaction on that database.  This should be used to ensure that operations
91      * across multiple databases are all blocked on a single DB lock (to prevent deadlock cases).
92      *
93      * Hint: It's always {@link ContactsDatabaseHelper}.
94      *
95      * TODO Change the structure to make it obvious that it's actually always set, and is the
96      * {@link ContactsDatabaseHelper}.
97      */
98     private SQLiteOpenHelper mSerializeOnDbHelper;
99 
100     /**
101      * The tag corresponding to the database used for serializing transactions.
102      *
103      * Hint: It's always the contacts db helper tag.
104      *
105      * See also the TODO on {@link #mSerializeOnDbHelper}.
106      */
107     private String mSerializeDbTag;
108 
109     /**
110      * The transaction listener used with {@link #mSerializeOnDbHelper}.
111      *
112      * Hint: It's always {@link ContactsProvider2}.
113      *
114      * See also the TODO on {@link #mSerializeOnDbHelper}.
115      */
116     private SQLiteTransactionListener mSerializedDbTransactionListener;
117 
118 
119     protected final ProviderAccessStats mStats = new ProviderAccessStats();
120 
121     @Override
onCreate()122     public boolean onCreate() {
123         Context context = getContext();
124         mDbHelper = newDatabaseHelper(context);
125         mTransactionHolder = getTransactionHolder();
126         return true;
127     }
128 
getDatabaseHelper()129     public ContactsDatabaseHelper getDatabaseHelper() {
130         return mDbHelper;
131     }
132 
133     /**
134      * Specifies a database helper (and corresponding tag) to serialize all transactions on.
135      *
136      * See also the TODO on {@link #mSerializeOnDbHelper}.
137      */
setDbHelperToSerializeOn(SQLiteOpenHelper serializeOnDbHelper, String tag, SQLiteTransactionListener listener)138     public void setDbHelperToSerializeOn(SQLiteOpenHelper serializeOnDbHelper, String tag,
139             SQLiteTransactionListener listener) {
140         mSerializeOnDbHelper = serializeOnDbHelper;
141         mSerializeDbTag = tag;
142         mSerializedDbTransactionListener = listener;
143     }
144 
getCurrentTransaction()145     public ContactsTransaction getCurrentTransaction() {
146         return mTransactionHolder.get();
147     }
148 
isInBatch()149     private boolean isInBatch() {
150         final ContactsTransaction t = mTransactionHolder.get();
151         return t != null && t.isBatch();
152     }
153 
154     @Override
insert(Uri uri, ContentValues values)155     public Uri insert(Uri uri, ContentValues values) {
156         final int callingUid = Binder.getCallingUid();
157         mStats.incrementInsertStats(callingUid, isInBatch());
158         try {
159             ContactsTransaction transaction = startTransaction(false);
160             try {
161                 Uri result = insertInTransaction(uri, values);
162                 if (result != null) {
163                     transaction.markDirty();
164                 }
165                 transaction.markSuccessful(false);
166                 return result;
167             } finally {
168                 endTransaction(false);
169             }
170         } finally {
171             mStats.finishOperation(callingUid);
172         }
173     }
174 
175     @Override
delete(Uri uri, String selection, String[] selectionArgs)176     public int delete(Uri uri, String selection, String[] selectionArgs) {
177         final int callingUid = Binder.getCallingUid();
178         mStats.incrementDeleteStats(callingUid, isInBatch());
179         try {
180             ContactsTransaction transaction = startTransaction(false);
181             try {
182                 int deleted = deleteInTransaction(uri, selection, selectionArgs);
183                 if (deleted > 0) {
184                     transaction.markDirty();
185                 }
186                 transaction.markSuccessful(false);
187                 return deleted;
188             } finally {
189                 endTransaction(false);
190             }
191         } finally {
192             mStats.finishOperation(callingUid);
193         }
194     }
195 
196     @Override
update(Uri uri, ContentValues values, String selection, String[] selectionArgs)197     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
198         final int callingUid = Binder.getCallingUid();
199         mStats.incrementUpdateStats(callingUid, isInBatch());
200         try {
201             ContactsTransaction transaction = startTransaction(false);
202             try {
203                 int updated = updateInTransaction(uri, values, selection, selectionArgs);
204                 if (updated > 0) {
205                     transaction.markDirty();
206                 }
207                 transaction.markSuccessful(false);
208                 return updated;
209             } finally {
210                 endTransaction(false);
211             }
212         } finally {
213             mStats.finishOperation(callingUid);
214         }
215     }
216 
217     @Override
bulkInsert(Uri uri, ContentValues[] values)218     public int bulkInsert(Uri uri, ContentValues[] values) {
219         final int callingUid = Binder.getCallingUid();
220         mStats.incrementBatchStats(callingUid);
221         try {
222             ContactsTransaction transaction = startTransaction(true);
223             int numValues = values.length;
224             int opCount = 0;
225             try {
226                 for (int i = 0; i < numValues; i++) {
227                     insert(uri, values[i]);
228                     if (++opCount >= BULK_INSERTS_PER_YIELD_POINT) {
229                         opCount = 0;
230                         try {
231                             yield(transaction);
232                         } catch (RuntimeException re) {
233                             transaction.markYieldFailed();
234                             throw re;
235                         }
236                     }
237                 }
238                 transaction.markSuccessful(true);
239             } finally {
240                 endTransaction(true);
241             }
242             return numValues;
243         } finally {
244             mStats.finishOperation(callingUid);
245         }
246     }
247 
248     @Override
applyBatch(ArrayList<ContentProviderOperation> operations)249     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
250             throws OperationApplicationException {
251         final int callingUid = Binder.getCallingUid();
252         mStats.incrementBatchStats(callingUid);
253         try {
254             if (VERBOSE_LOGGING) {
255                 Log.v(TAG, "applyBatch: " + operations.size() + " ops");
256             }
257             int ypCount = 0;
258             int opCount = 0;
259             ContactsTransaction transaction = startTransaction(true);
260             try {
261                 final int numOperations = operations.size();
262                 final ContentProviderResult[] results = new ContentProviderResult[numOperations];
263                 for (int i = 0; i < numOperations; i++) {
264                     if (++opCount >= MAX_OPERATIONS_PER_YIELD_POINT) {
265                         throw new OperationApplicationException(
266                                 "Too many content provider operations between yield points. "
267                                         + "The maximum number of operations per yield point is "
268                                         + MAX_OPERATIONS_PER_YIELD_POINT, ypCount);
269                     }
270                     final ContentProviderOperation operation = operations.get(i);
271                     if (i > 0 && operation.isYieldAllowed()) {
272                         if (VERBOSE_LOGGING) {
273                             Log.v(TAG, "applyBatch: " + opCount + " ops finished; about to yield...");
274                         }
275                         opCount = 0;
276                         try {
277                             if (yield(transaction)) {
278                                 ypCount++;
279                             }
280                         } catch (RuntimeException re) {
281                             transaction.markYieldFailed();
282                             throw re;
283                         }
284                     }
285 
286                     results[i] = operation.apply(this, results, i);
287                 }
288                 transaction.markSuccessful(true);
289                 return results;
290             } finally {
291                 endTransaction(true);
292             }
293         } finally {
294             mStats.finishOperation(callingUid);
295         }
296     }
297 
298     /**
299      * If we are not yet already in a transaction, this starts one (on the DB to serialize on, if
300      * present) and sets the thread-local transaction variable for tracking.  If we are already in
301      * a transaction, this returns that transaction, and the batch parameter is ignored.
302      * @param callerIsBatch Whether the caller is operating in batch mode.
303      */
startTransaction(boolean callerIsBatch)304     private ContactsTransaction startTransaction(boolean callerIsBatch) {
305         if (ENABLE_TRANSACTION_LOG) {
306             Log.i(TAG, "startTransaction " + getClass().getSimpleName() +
307                     "  callerIsBatch=" + callerIsBatch, new RuntimeException("startTransaction"));
308         }
309         ContactsTransaction transaction = mTransactionHolder.get();
310         if (transaction == null) {
311             transaction = new ContactsTransaction(callerIsBatch);
312             if (mSerializeOnDbHelper != null) {
313                 transaction.startTransactionForDb(mSerializeOnDbHelper.getWritableDatabase(),
314                         mSerializeDbTag, mSerializedDbTransactionListener);
315             }
316             mTransactionHolder.set(transaction);
317         }
318         return transaction;
319     }
320 
321     /**
322      * Ends the current transaction and clears out the member variable.  This does not set the
323      * transaction as being successful.
324      * @param callerIsBatch Whether the caller is operating in batch mode.
325      */
endTransaction(boolean callerIsBatch)326     private void endTransaction(boolean callerIsBatch) {
327         if (ENABLE_TRANSACTION_LOG) {
328             Log.i(TAG, "endTransaction " + getClass().getSimpleName() +
329                     "  callerIsBatch=" + callerIsBatch, new RuntimeException("endTransaction"));
330         }
331         ContactsTransaction transaction = mTransactionHolder.get();
332         if (transaction != null && (!transaction.isBatch() || callerIsBatch)) {
333             boolean notify = false;
334             try {
335                 if (transaction.isDirty()) {
336                     notify = true;
337                 }
338                 transaction.finish(callerIsBatch);
339                 if (notify) {
340                     notifyChange();
341                 }
342             } finally {
343                 // No matter what, make sure we clear out the thread-local transaction reference.
344                 mTransactionHolder.set(null);
345             }
346         }
347     }
348 
349     /**
350      * Gets the database helper for this contacts provider.  This is called once, during onCreate().
351      * Do not call in other places.
352      */
newDatabaseHelper(Context context)353     protected abstract ContactsDatabaseHelper newDatabaseHelper(Context context);
354 
355     /**
356      * Gets the thread-local transaction holder to use for keeping track of the transaction.  This
357      * is called once, in onCreate().  If multiple classes are inheriting from this class that need
358      * to be kept in sync on the same transaction, they must all return the same thread-local.
359      */
getTransactionHolder()360     protected abstract ThreadLocal<ContactsTransaction> getTransactionHolder();
361 
insertInTransaction(Uri uri, ContentValues values)362     protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
363 
deleteInTransaction(Uri uri, String selection, String[] selectionArgs)364     protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
365 
updateInTransaction(Uri uri, ContentValues values, String selection, String[] selectionArgs)366     protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
367             String[] selectionArgs);
368 
yield(ContactsTransaction transaction)369     protected abstract boolean yield(ContactsTransaction transaction);
370 
notifyChange()371     protected abstract void notifyChange();
372 
373     private static final String ACCOUNTS_QUERY =
374             "SELECT * FROM " + Tables.ACCOUNTS + " ORDER BY " + BaseColumns._ID;
375 
376     private static final String NUM_INVISIBLE_CONTACTS_QUERY =
377             "SELECT count(*) FROM " + Tables.CONTACTS;
378 
379     private static final String NUM_VISIBLE_CONTACTS_QUERY =
380             "SELECT count(*) FROM " + Tables.DEFAULT_DIRECTORY;
381 
382     private static final String NUM_RAW_CONTACTS_PER_CONTACT =
383             "SELECT _id, count(*) as c FROM " + Tables.RAW_CONTACTS
384                     + " GROUP BY " + RawContacts.CONTACT_ID;
385 
386     private static final String MAX_RAW_CONTACTS_PER_CONTACT =
387             "SELECT max(c) FROM (" + NUM_RAW_CONTACTS_PER_CONTACT + ")";
388 
389     private static final String AVG_RAW_CONTACTS_PER_CONTACT =
390             "SELECT avg(c) FROM (" + NUM_RAW_CONTACTS_PER_CONTACT + ")";
391 
392     private static final String NUM_RAW_CONTACT_PER_ACCOUNT_PER_CONTACT =
393             "SELECT " + RawContactsColumns.ACCOUNT_ID + " AS aid"
394                     + ", " + RawContacts.CONTACT_ID + " AS cid"
395                     + ", count(*) AS c"
396                     + " FROM " + Tables.RAW_CONTACTS
397                     + " GROUP BY aid, cid";
398 
399     private static final String RAW_CONTACTS_PER_ACCOUNT_PER_CONTACT =
400             "SELECT aid, sum(c) AS s, max(c) AS m, avg(c) AS a"
401                     + " FROM (" + NUM_RAW_CONTACT_PER_ACCOUNT_PER_CONTACT + ")"
402                     + " GROUP BY aid";
403 
404     private static final String DATA_WITH_ACCOUNT =
405             "SELECT d._id AS did"
406             + ", d." + Data.RAW_CONTACT_ID + " AS rid"
407             + ", r." + RawContactsColumns.ACCOUNT_ID + " AS aid"
408             + " FROM " + Tables.DATA + " AS d JOIN " + Tables.RAW_CONTACTS + " AS r"
409             + " ON d." + Data.RAW_CONTACT_ID + "=r._id";
410 
411     private static final String NUM_DATA_PER_ACCOUNT_PER_RAW_CONTACT =
412             "SELECT aid, rid, count(*) AS c"
413                     + " FROM (" + DATA_WITH_ACCOUNT + ")"
414                     + " GROUP BY aid, rid";
415 
416     private static final String DATA_PER_ACCOUNT_PER_RAW_CONTACT =
417             "SELECT aid, sum(c) AS s, max(c) AS m, avg(c) AS a"
418                     + " FROM (" + NUM_DATA_PER_ACCOUNT_PER_RAW_CONTACT + ")"
419                     + " GROUP BY aid";
420 
dump(PrintWriter pw, String dbName)421     protected void dump(PrintWriter pw, String dbName) {
422         pw.print("Database: ");
423         pw.println(dbName);
424 
425         mStats.dump(pw, "  ");
426 
427         if (mDbHelper == null) {
428             pw.println("mDbHelper is null");
429             return;
430         }
431         try {
432             pw.println();
433             pw.println("  Accounts:");
434             final SQLiteDatabase db = mDbHelper.getReadableDatabase();
435 
436             try (Cursor c = db.rawQuery(ACCOUNTS_QUERY, null)) {
437                 c.moveToPosition(-1);
438                 while (c.moveToNext()) {
439                     pw.print("    ");
440                     dumpLongColumn(pw, c, BaseColumns._ID);
441                     pw.print(" ");
442                     dumpStringColumn(pw, c, AccountsColumns.ACCOUNT_NAME);
443                     pw.print(" ");
444                     dumpStringColumn(pw, c, AccountsColumns.ACCOUNT_TYPE);
445                     pw.print(" ");
446                     dumpStringColumn(pw, c, AccountsColumns.DATA_SET);
447                     pw.println();
448                 }
449             }
450 
451             pw.println();
452             pw.println("  Contacts:");
453             pw.print("    # of visible: ");
454             pw.print(longForQuery(db, NUM_VISIBLE_CONTACTS_QUERY));
455             pw.println();
456 
457             pw.print("    # of invisible: ");
458             pw.print(longForQuery(db, NUM_INVISIBLE_CONTACTS_QUERY));
459             pw.println();
460 
461             pw.print("    Max # of raw contacts: ");
462             pw.print(longForQuery(db, MAX_RAW_CONTACTS_PER_CONTACT));
463             pw.println();
464 
465             pw.print("    Avg # of raw contacts: ");
466             pw.print(doubleForQuery(db, AVG_RAW_CONTACTS_PER_CONTACT));
467             pw.println();
468 
469             pw.println();
470             pw.println("  Raw contacts (per account):");
471             try (Cursor c = db.rawQuery(RAW_CONTACTS_PER_ACCOUNT_PER_CONTACT, null)) {
472                 c.moveToPosition(-1);
473                 while (c.moveToNext()) {
474                     pw.print("    ");
475                     dumpLongColumn(pw, c, "aid");
476                     pw.print(" total # of raw contacts: ");
477                     dumpStringColumn(pw, c, "s");
478                     pw.print(", max # per contact: ");
479                     dumpLongColumn(pw, c, "m");
480                     pw.print(", avg # per contact: ");
481                     dumpDoubleColumn(pw, c, "a");
482                     pw.println();
483                 }
484             }
485 
486             pw.println();
487             pw.println("  Data (per account):");
488             try (Cursor c = db.rawQuery(DATA_PER_ACCOUNT_PER_RAW_CONTACT, null)) {
489                 c.moveToPosition(-1);
490                 while (c.moveToNext()) {
491                     pw.print("    ");
492                     dumpLongColumn(pw, c, "aid");
493                     pw.print(" total # of data:");
494                     dumpLongColumn(pw, c, "s");
495                     pw.print(", max # per raw contact: ");
496                     dumpLongColumn(pw, c, "m");
497                     pw.print(", avg # per raw contact: ");
498                     dumpDoubleColumn(pw, c, "a");
499                     pw.println();
500                 }
501             }
502         } catch (Exception e) {
503             pw.println("Error: " + e);
504         }
505     }
506 
dumpStringColumn(PrintWriter pw, Cursor c, String column)507     private static void dumpStringColumn(PrintWriter pw, Cursor c, String column) {
508         final int index = c.getColumnIndex(column);
509         if (index == -1) {
510             pw.println("Column not found: " + column);
511             return;
512         }
513         final String value = c.getString(index);
514         if (value == null) {
515             pw.print("(null)");
516         } else if (value.length() == 0) {
517             pw.print("\"\"");
518         } else {
519             pw.print(value);
520         }
521     }
522 
dumpLongColumn(PrintWriter pw, Cursor c, String column)523     private static void dumpLongColumn(PrintWriter pw, Cursor c, String column) {
524         final int index = c.getColumnIndex(column);
525         if (index == -1) {
526             pw.println("Column not found: " + column);
527             return;
528         }
529         if (c.isNull(index)) {
530             pw.print("(null)");
531         } else {
532             pw.print(c.getLong(index));
533         }
534     }
535 
dumpDoubleColumn(PrintWriter pw, Cursor c, String column)536     private static void dumpDoubleColumn(PrintWriter pw, Cursor c, String column) {
537         final int index = c.getColumnIndex(column);
538         if (index == -1) {
539             pw.println("Column not found: " + column);
540             return;
541         }
542         if (c.isNull(index)) {
543             pw.print("(null)");
544         } else {
545             pw.print(c.getDouble(index));
546         }
547     }
548 
longForQuery(SQLiteDatabase db, String query)549     private static long longForQuery(SQLiteDatabase db, String query) {
550         return DatabaseUtils.longForQuery(db, query, null);
551     }
552 
doubleForQuery(SQLiteDatabase db, String query)553     private static double doubleForQuery(SQLiteDatabase db, String query) {
554         try (final Cursor c = db.rawQuery(query, null)) {
555             if (!c.moveToFirst()) {
556                 return -1;
557             }
558             return c.getDouble(0);
559         }
560     }
561 }
562