Home | History | Annotate | Download | only in contacts
      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
    134     public boolean onCreate() {
    135         Context context = getContext();
    136         mDbHelper = getDatabaseHelper(context);
    137         mTransactionHolder = getTransactionHolder();
    138         return true;
    139     }
    140 
    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      */
    150     public void setDbHelperToSerializeOn(SQLiteOpenHelper serializeOnDbHelper, String tag,
    151             SQLiteTransactionListener listener) {
    152         mSerializeOnDbHelper = serializeOnDbHelper;
    153         mSerializeDbTag = tag;
    154         mSerializedDbTransactionListener = listener;
    155     }
    156 
    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 
    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 
    172     public ContactsTransaction getCurrentTransaction() {
    173         return mTransactionHolder.get();
    174     }
    175 
    176     @Override
    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
    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
    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
    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
    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      */
    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      */
    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      */
    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      */
    356     protected abstract ThreadLocal<ContactsTransaction> getTransactionHolder();
    357 
    358     protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
    359 
    360     protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
    361 
    362     protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
    363             String[] selectionArgs);
    364 
    365     protected abstract boolean yield(ContactsTransaction transaction);
    366 
    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 
    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 
    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 
    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 
    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 
    568     private static long longForQuery(SQLiteDatabase db, String query) {
    569         return DatabaseUtils.longForQuery(db, query, null);
    570     }
    571 
    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