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