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