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 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
    122     public boolean onCreate() {
    123         Context context = getContext();
    124         mDbHelper = newDatabaseHelper(context);
    125         mTransactionHolder = getTransactionHolder();
    126         return true;
    127     }
    128 
    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      */
    138     public void setDbHelperToSerializeOn(SQLiteOpenHelper serializeOnDbHelper, String tag,
    139             SQLiteTransactionListener listener) {
    140         mSerializeOnDbHelper = serializeOnDbHelper;
    141         mSerializeDbTag = tag;
    142         mSerializedDbTransactionListener = listener;
    143     }
    144 
    145     public ContactsTransaction getCurrentTransaction() {
    146         return mTransactionHolder.get();
    147     }
    148 
    149     private boolean isInBatch() {
    150         final ContactsTransaction t = mTransactionHolder.get();
    151         return t != null && t.isBatch();
    152     }
    153 
    154     @Override
    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
    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
    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
    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
    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      */
    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      */
    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      */
    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      */
    360     protected abstract ThreadLocal<ContactsTransaction> getTransactionHolder();
    361 
    362     protected abstract Uri insertInTransaction(Uri uri, ContentValues values);
    363 
    364     protected abstract int deleteInTransaction(Uri uri, String selection, String[] selectionArgs);
    365 
    366     protected abstract int updateInTransaction(Uri uri, ContentValues values, String selection,
    367             String[] selectionArgs);
    368 
    369     protected abstract boolean yield(ContactsTransaction transaction);
    370 
    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 
    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 
    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 
    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 
    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 
    549     private static long longForQuery(SQLiteDatabase db, String query) {
    550         return DatabaseUtils.longForQuery(db, query, null);
    551     }
    552 
    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