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