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