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