1 /*
2  * Copyright (C) 2019 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.media.util;
18 
19 import static android.content.ContentResolver.QUERY_ARG_GROUP_COLUMNS;
20 import static android.content.ContentResolver.QUERY_ARG_LIMIT;
21 import static android.content.ContentResolver.QUERY_ARG_OFFSET;
22 import static android.content.ContentResolver.QUERY_ARG_SORT_COLLATION;
23 import static android.content.ContentResolver.QUERY_ARG_SORT_COLUMNS;
24 import static android.content.ContentResolver.QUERY_ARG_SORT_DIRECTION;
25 import static android.content.ContentResolver.QUERY_ARG_SORT_LOCALE;
26 import static android.content.ContentResolver.QUERY_ARG_SQL_GROUP_BY;
27 import static android.content.ContentResolver.QUERY_ARG_SQL_LIMIT;
28 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION;
29 import static android.content.ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS;
30 import static android.content.ContentResolver.QUERY_ARG_SQL_SORT_ORDER;
31 import static android.content.ContentResolver.QUERY_SORT_DIRECTION_ASCENDING;
32 import static android.content.ContentResolver.QUERY_SORT_DIRECTION_DESCENDING;
33 
34 import static com.android.providers.media.util.Logging.TAG;
35 
36 import android.content.ContentResolver;
37 import android.content.ContentValues;
38 import android.database.Cursor;
39 import android.database.SQLException;
40 import android.database.sqlite.SQLiteDatabase;
41 import android.database.sqlite.SQLiteStatement;
42 import android.net.Uri;
43 import android.os.Bundle;
44 import android.os.Trace;
45 import android.text.TextUtils;
46 import android.util.ArrayMap;
47 import android.util.Log;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 
52 import java.util.Locale;
53 import java.util.function.Consumer;
54 import java.util.function.Function;
55 
56 public class DatabaseUtils {
57     /**
58      * Bind the given selection with the given selection arguments.
59      * <p>
60      * Internally assumes that '?' is only ever used for arguments, and doesn't
61      * appear as a literal or escaped value.
62      * <p>
63      * This method is typically useful for trusted code that needs to cook up a
64      * fully-bound selection.
65      *
66      * @hide
67      */
bindSelection(@ullable String selection, @Nullable Object... selectionArgs)68     public static @Nullable String bindSelection(@Nullable String selection,
69             @Nullable Object... selectionArgs) {
70         if (selection == null) return null;
71         // If no arguments provided, so we can't bind anything
72         if ((selectionArgs == null) || (selectionArgs.length == 0)) return selection;
73         // If no bindings requested, so we can shortcut
74         if (selection.indexOf('?') == -1) return selection;
75 
76         // Track the chars immediately before and after each bind request, to
77         // decide if it needs additional whitespace added
78         char before = ' ';
79         char after = ' ';
80 
81         int argIndex = 0;
82         final int len = selection.length();
83         final StringBuilder res = new StringBuilder(len);
84         for (int i = 0; i < len; ) {
85             char c = selection.charAt(i++);
86             if (c == '?') {
87                 // Assume this bind request is guarded until we find a specific
88                 // trailing character below
89                 after = ' ';
90 
91                 // Sniff forward to see if the selection is requesting a
92                 // specific argument index
93                 int start = i;
94                 for (; i < len; i++) {
95                     c = selection.charAt(i);
96                     if (c < '0' || c > '9') {
97                         after = c;
98                         break;
99                     }
100                 }
101                 if (start != i) {
102                     argIndex = Integer.parseInt(selection.substring(start, i)) - 1;
103                 }
104 
105                 // Manually bind the argument into the selection, adding
106                 // whitespace when needed for clarity
107                 final Object arg = selectionArgs[argIndex++];
108                 if (before != ' ' && before != '=') res.append(' ');
109                 switch (DatabaseUtils.getTypeOfObject(arg)) {
110                     case Cursor.FIELD_TYPE_NULL:
111                         res.append("NULL");
112                         break;
113                     case Cursor.FIELD_TYPE_INTEGER:
114                         res.append(((Number) arg).longValue());
115                         break;
116                     case Cursor.FIELD_TYPE_FLOAT:
117                         res.append(((Number) arg).doubleValue());
118                         break;
119                     case Cursor.FIELD_TYPE_BLOB:
120                         throw new IllegalArgumentException("Blobs not supported");
121                     case Cursor.FIELD_TYPE_STRING:
122                     default:
123                         if (arg instanceof Boolean) {
124                             // Provide compatibility with legacy applications which may pass
125                             // Boolean values in bind args.
126                             res.append(((Boolean) arg).booleanValue() ? 1 : 0);
127                         } else {
128                             res.append('\'');
129                             // Escape single quote character while appending the string and reject
130                             // invalid unicode.
131                             res.append(escapeSingleQuoteAndRejectInvalidUnicode(arg.toString()));
132                             res.append('\'');
133                         }
134                         break;
135                 }
136                 if (after != ' ') res.append(' ');
137             } else {
138                 res.append(c);
139                 before = c;
140             }
141         }
142         return res.toString();
143     }
144 
escapeSingleQuoteAndRejectInvalidUnicode(@onNull String target)145     private static String escapeSingleQuoteAndRejectInvalidUnicode(@NonNull String target) {
146         final int len = target.length();
147         final StringBuilder res = new StringBuilder(len);
148         boolean lastHigh = false;
149 
150         for (int i = 0; i < len; ) {
151             final char c = target.charAt(i++);
152 
153             if (lastHigh != Character.isLowSurrogate(c)) {
154                 Log.e(TAG, "Invalid surrogate in string " + target);
155                 throw new IllegalArgumentException("Invalid surrogate in string " + target);
156             }
157 
158             lastHigh = Character.isHighSurrogate(c);
159 
160             // Escape the single quotes by duplicating them
161             if (c == '\'') {
162                 res.append(c);
163             }
164 
165             res.append(c);
166         }
167 
168         if (lastHigh) {
169             Log.e(TAG, "Invalid surrogate in string " + target);
170             throw new IllegalArgumentException("Invalid surrogate in string " + target);
171         }
172 
173         return res.toString();
174     }
175 
176     /**
177      * Returns data type of the given object's value.
178      *<p>
179      * Returned values are
180      * <ul>
181      *   <li>{@link Cursor#FIELD_TYPE_NULL}</li>
182      *   <li>{@link Cursor#FIELD_TYPE_INTEGER}</li>
183      *   <li>{@link Cursor#FIELD_TYPE_FLOAT}</li>
184      *   <li>{@link Cursor#FIELD_TYPE_STRING}</li>
185      *   <li>{@link Cursor#FIELD_TYPE_BLOB}</li>
186      *</ul>
187      *</p>
188      *
189      * @param obj the object whose value type is to be returned
190      * @return object value type
191      * @hide
192      */
getTypeOfObject(Object obj)193     public static int getTypeOfObject(Object obj) {
194         if (obj == null) {
195             return Cursor.FIELD_TYPE_NULL;
196         } else if (obj instanceof byte[]) {
197             return Cursor.FIELD_TYPE_BLOB;
198         } else if (obj instanceof Float || obj instanceof Double) {
199             return Cursor.FIELD_TYPE_FLOAT;
200         } else if (obj instanceof Long || obj instanceof Integer
201                 || obj instanceof Short || obj instanceof Byte) {
202             return Cursor.FIELD_TYPE_INTEGER;
203         } else {
204             return Cursor.FIELD_TYPE_STRING;
205         }
206     }
207 
copyFromCursorToContentValues(@onNull String column, @NonNull Cursor cursor, @NonNull ContentValues values)208     public static void copyFromCursorToContentValues(@NonNull String column, @NonNull Cursor cursor,
209             @NonNull ContentValues values) {
210         final int index = cursor.getColumnIndex(column);
211         if (index != -1) {
212             if (cursor.isNull(index)) {
213                 values.putNull(column);
214             } else {
215                 values.put(column, cursor.getString(index));
216             }
217         }
218     }
219 
220     /**
221      * Simple attempt to balance the given SQL expression by adding parenthesis
222      * when needed.
223      * <p>
224      * Since this is only used for recovering from abusive apps, we're not
225      * interested in trying to build a fully valid SQL parser up in Java. It'll
226      * give up when it encounters complex SQL, such as string literals.
227      */
maybeBalance(@ullable String sql)228     public static @Nullable String maybeBalance(@Nullable String sql) {
229         if (sql == null) return null;
230 
231         int count = 0;
232         char literal = '\0';
233         for (int i = 0; i < sql.length(); i++) {
234             final char c = sql.charAt(i);
235 
236             if (c == '\'' || c == '"') {
237                 if (literal == '\0') {
238                     // Start literal
239                     literal = c;
240                 } else if (literal == c) {
241                     // End literal
242                     literal = '\0';
243                 }
244             }
245 
246             if (literal == '\0') {
247                 if (c == '(') {
248                     count++;
249                 } else if (c == ')') {
250                     count--;
251                 }
252             }
253         }
254         while (count > 0) {
255             sql = sql + ")";
256             count--;
257         }
258         while (count < 0) {
259             sql = "(" + sql;
260             count++;
261         }
262         return sql;
263     }
264 
265     /**
266      * {@link ContentResolver} offers several query arguments, ranging from
267      * helpful higher-level concepts like
268      * {@link ContentResolver#QUERY_ARG_GROUP_COLUMNS} to raw SQL like
269      * {@link ContentResolver#QUERY_ARG_SQL_GROUP_BY}. We prefer the
270      * higher-level concepts when defined by the caller, but we'll fall back to
271      * the raw SQL if that's all the caller provided.
272      * <p>
273      * This method will "resolve" all higher-level query arguments into the raw
274      * SQL arguments, giving us easy values to carry over into
275      * {@link SQLiteQueryBuilder}.
276      */
resolveQueryArgs(@onNull Bundle queryArgs, @NonNull Consumer<String> honored, @NonNull Function<String, String> collatorFactory)277     public static void resolveQueryArgs(@NonNull Bundle queryArgs,
278             @NonNull Consumer<String> honored,
279             @NonNull Function<String, String> collatorFactory) {
280         // We're always going to handle selections
281         honored.accept(QUERY_ARG_SQL_SELECTION);
282         honored.accept(QUERY_ARG_SQL_SELECTION_ARGS);
283 
284         resolveGroupBy(queryArgs, honored);
285         resolveSortOrder(queryArgs, honored, collatorFactory);
286         resolveLimit(queryArgs, honored);
287     }
288 
resolveGroupBy(@onNull Bundle queryArgs, @NonNull Consumer<String> honored)289     private static void resolveGroupBy(@NonNull Bundle queryArgs,
290             @NonNull Consumer<String> honored) {
291         final String[] columns = queryArgs.getStringArray(QUERY_ARG_GROUP_COLUMNS);
292         if (columns != null && columns.length != 0) {
293             String groupBy = TextUtils.join(", ", columns);
294             honored.accept(QUERY_ARG_GROUP_COLUMNS);
295 
296             queryArgs.putString(QUERY_ARG_SQL_GROUP_BY, groupBy);
297         } else {
298             honored.accept(QUERY_ARG_SQL_GROUP_BY);
299         }
300     }
301 
resolveSortOrder(@onNull Bundle queryArgs, @NonNull Consumer<String> honored, @NonNull Function<String, String> collatorFactory)302     private static void resolveSortOrder(@NonNull Bundle queryArgs,
303             @NonNull Consumer<String> honored,
304             @NonNull Function<String, String> collatorFactory) {
305         final String[] columns = queryArgs.getStringArray(QUERY_ARG_SORT_COLUMNS);
306         if (columns != null && columns.length != 0) {
307             String sortOrder = TextUtils.join(", ", columns);
308             honored.accept(QUERY_ARG_SORT_COLUMNS);
309 
310             if (queryArgs.containsKey(QUERY_ARG_SORT_LOCALE)) {
311                 final String collatorName = collatorFactory.apply(
312                         queryArgs.getString(QUERY_ARG_SORT_LOCALE));
313                 sortOrder += " COLLATE " + collatorName;
314                 honored.accept(QUERY_ARG_SORT_LOCALE);
315             } else {
316                 // Interpret PRIMARY and SECONDARY collation strength as no-case collation based
317                 // on their javadoc descriptions.
318                 final int collation = queryArgs.getInt(
319                         QUERY_ARG_SORT_COLLATION, java.text.Collator.IDENTICAL);
320                 switch (collation) {
321                     case java.text.Collator.IDENTICAL:
322                         honored.accept(QUERY_ARG_SORT_COLLATION);
323                         break;
324                     case java.text.Collator.PRIMARY:
325                     case java.text.Collator.SECONDARY:
326                         sortOrder += " COLLATE NOCASE";
327                         honored.accept(QUERY_ARG_SORT_COLLATION);
328                         break;
329                 }
330             }
331 
332             final int sortDir = queryArgs.getInt(QUERY_ARG_SORT_DIRECTION, Integer.MIN_VALUE);
333             switch (sortDir) {
334                 case QUERY_SORT_DIRECTION_ASCENDING:
335                     sortOrder += " ASC";
336                     honored.accept(QUERY_ARG_SORT_DIRECTION);
337                     break;
338                 case QUERY_SORT_DIRECTION_DESCENDING:
339                     sortOrder += " DESC";
340                     honored.accept(QUERY_ARG_SORT_DIRECTION);
341                     break;
342             }
343 
344             queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER, sortOrder);
345         } else {
346             honored.accept(QUERY_ARG_SQL_SORT_ORDER);
347         }
348     }
349 
resolveLimit(@onNull Bundle queryArgs, @NonNull Consumer<String> honored)350     private static void resolveLimit(@NonNull Bundle queryArgs,
351             @NonNull Consumer<String> honored) {
352         final int limit = queryArgs.getInt(QUERY_ARG_LIMIT, Integer.MIN_VALUE);
353         if (limit != Integer.MIN_VALUE) {
354             String limitString = Integer.toString(limit);
355             honored.accept(QUERY_ARG_LIMIT);
356 
357             final int offset = queryArgs.getInt(QUERY_ARG_OFFSET, Integer.MIN_VALUE);
358             if (offset != Integer.MIN_VALUE) {
359                 limitString += " OFFSET " + offset;
360                 honored.accept(QUERY_ARG_OFFSET);
361             }
362 
363             queryArgs.putString(QUERY_ARG_SQL_LIMIT, limitString);
364         } else {
365             honored.accept(QUERY_ARG_SQL_LIMIT);
366         }
367     }
368 
369     /**
370      * Gracefully recover from abusive callers that are smashing limits into
371      * {@link Uri}.
372      */
recoverAbusiveLimit(@onNull Uri uri, @NonNull Bundle queryArgs)373     public static void recoverAbusiveLimit(@NonNull Uri uri, @NonNull Bundle queryArgs) {
374         final String origLimit = queryArgs.getString(QUERY_ARG_SQL_LIMIT);
375         final String uriLimit = uri.getQueryParameter("limit");
376 
377         if (!TextUtils.isEmpty(uriLimit)) {
378             // Yell if we already had a group by requested
379             if (!TextUtils.isEmpty(origLimit)) {
380                 throw new IllegalArgumentException(
381                         "Abusive '" + uriLimit + "' conflicts with requested '" + origLimit + "'");
382             }
383 
384             Log.w(TAG, "Recovered abusive '" + uriLimit + "' from '" + uri + "'");
385 
386             queryArgs.putString(QUERY_ARG_SQL_LIMIT, uriLimit);
387         }
388     }
389 
390     /**
391      * Gracefully recover from abusive callers that are smashing invalid
392      * {@code GROUP BY} clauses into {@code WHERE} clauses.
393      */
recoverAbusiveSelection(@onNull Bundle queryArgs)394     public static void recoverAbusiveSelection(@NonNull Bundle queryArgs) {
395         final String origSelection = queryArgs.getString(QUERY_ARG_SQL_SELECTION);
396         final String origGroupBy = queryArgs.getString(QUERY_ARG_SQL_GROUP_BY);
397 
398         final int index = (origSelection != null)
399                 ? origSelection.toUpperCase(Locale.ROOT).indexOf(" GROUP BY ") : -1;
400         if (index != -1) {
401             String selection = origSelection.substring(0, index);
402             String groupBy = origSelection.substring(index + " GROUP BY ".length());
403 
404             // Try balancing things out
405             selection = maybeBalance(selection);
406             groupBy = maybeBalance(groupBy);
407 
408             // Yell if we already had a group by requested
409             if (!TextUtils.isEmpty(origGroupBy)) {
410                 throw new IllegalArgumentException(
411                         "Abusive '" + groupBy + "' conflicts with requested '" + origGroupBy + "'");
412             }
413 
414             Log.w(TAG, "Recovered abusive '" + selection + "' and '" + groupBy + "' from '"
415                     + origSelection + "'");
416 
417             queryArgs.putString(QUERY_ARG_SQL_SELECTION, selection);
418             queryArgs.putString(QUERY_ARG_SQL_GROUP_BY, groupBy);
419         }
420     }
421 
422     /**
423      * Gracefully recover from abusive callers that are smashing limits into
424      * {@code ORDER BY} clauses.
425      */
recoverAbusiveSortOrder(@onNull Bundle queryArgs)426     public static void recoverAbusiveSortOrder(@NonNull Bundle queryArgs) {
427         final String origSortOrder = queryArgs.getString(QUERY_ARG_SQL_SORT_ORDER);
428         final String origLimit = queryArgs.getString(QUERY_ARG_SQL_LIMIT);
429 
430         final int index = (origSortOrder != null)
431                 ? origSortOrder.toUpperCase(Locale.ROOT).indexOf(" LIMIT ") : -1;
432         if (index != -1) {
433             String sortOrder = origSortOrder.substring(0, index);
434             String limit = origSortOrder.substring(index + " LIMIT ".length());
435 
436             // Yell if we already had a limit requested
437             if (!TextUtils.isEmpty(origLimit)) {
438                 throw new IllegalArgumentException(
439                         "Abusive '" + limit + "' conflicts with requested '" + origLimit + "'");
440             }
441 
442             Log.w(TAG, "Recovered abusive '" + sortOrder + "' and '" + limit + "' from '"
443                     + origSortOrder + "'");
444 
445             queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER, sortOrder);
446             queryArgs.putString(QUERY_ARG_SQL_LIMIT, limit);
447         }
448     }
449 
450     /**
451      * Shamelessly borrowed from {@link ContentResolver}.
452      */
createSqlQueryBundle( @ullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder)453     public static @Nullable Bundle createSqlQueryBundle(
454             @Nullable String selection,
455             @Nullable String[] selectionArgs,
456             @Nullable String sortOrder) {
457 
458         if (selection == null && selectionArgs == null && sortOrder == null) {
459             return null;
460         }
461 
462         Bundle queryArgs = new Bundle();
463         if (selection != null) {
464             queryArgs.putString(QUERY_ARG_SQL_SELECTION, selection);
465         }
466         if (selectionArgs != null) {
467             queryArgs.putStringArray(QUERY_ARG_SQL_SELECTION_ARGS, selectionArgs);
468         }
469         if (sortOrder != null) {
470             queryArgs.putString(QUERY_ARG_SQL_SORT_ORDER, sortOrder);
471         }
472         return queryArgs;
473     }
474 
getValues(@onNull ContentValues values)475     public static @NonNull ArrayMap<String, Object> getValues(@NonNull ContentValues values) {
476         final ArrayMap<String, Object> res = new ArrayMap<>();
477         for (String key : values.keySet()) {
478             res.put(key, values.get(key));
479         }
480         return res;
481     }
482 
executeInsert(@onNull SQLiteDatabase db, @NonNull String sql, @Nullable Object[] bindArgs)483     public static long executeInsert(@NonNull SQLiteDatabase db, @NonNull String sql,
484             @Nullable Object[] bindArgs) throws SQLException {
485         Trace.beginSection("DbUtils.executeInsert");
486         try (SQLiteStatement st = db.compileStatement(sql)) {
487             bindArgs(st, bindArgs);
488             return st.executeInsert();
489         } finally {
490             Trace.endSection();
491         }
492     }
493 
executeUpdateDelete(@onNull SQLiteDatabase db, @NonNull String sql, @Nullable Object[] bindArgs)494     public static int executeUpdateDelete(@NonNull SQLiteDatabase db, @NonNull String sql,
495             @Nullable Object[] bindArgs) throws SQLException {
496         Trace.beginSection("DbUtils.executeUpdateDelete");
497         try (SQLiteStatement st = db.compileStatement(sql)) {
498             bindArgs(st, bindArgs);
499             return st.executeUpdateDelete();
500         } finally {
501             Trace.endSection();
502         }
503     }
504 
bindArgs(@onNull SQLiteStatement st, @Nullable Object[] bindArgs)505     private static void bindArgs(@NonNull SQLiteStatement st, @Nullable Object[] bindArgs) {
506         if (bindArgs == null) return;
507 
508         for (int i = 0; i < bindArgs.length; i++) {
509             final Object bindArg = bindArgs[i];
510             switch (getTypeOfObject(bindArg)) {
511                 case Cursor.FIELD_TYPE_NULL:
512                     st.bindNull(i + 1);
513                     break;
514                 case Cursor.FIELD_TYPE_INTEGER:
515                     st.bindLong(i + 1, ((Number) bindArg).longValue());
516                     break;
517                 case Cursor.FIELD_TYPE_FLOAT:
518                     st.bindDouble(i + 1, ((Number) bindArg).doubleValue());
519                     break;
520                 case Cursor.FIELD_TYPE_BLOB:
521                     st.bindBlob(i + 1, (byte[]) bindArg);
522                     break;
523                 case Cursor.FIELD_TYPE_STRING:
524                 default:
525                     if (bindArg instanceof Boolean) {
526                         // Provide compatibility with legacy
527                         // applications which may pass Boolean values in
528                         // bind args.
529                         st.bindLong(i + 1, ((Boolean) bindArg).booleanValue() ? 1 : 0);
530                     } else {
531                         st.bindString(i + 1, bindArg.toString());
532                     }
533                     break;
534             }
535         }
536     }
537 
bindList(@onNull Object... args)538     public static @NonNull String bindList(@NonNull Object... args) {
539         final StringBuilder sb = new StringBuilder();
540         sb.append('(');
541         for (int i = 0; i < args.length; i++) {
542             sb.append('?');
543             if (i < args.length - 1) {
544                 sb.append(',');
545             }
546         }
547         sb.append(')');
548         return DatabaseUtils.bindSelection(sb.toString(), args);
549     }
550 
551     /**
552      * Escape the given argument for use in a {@code LIKE} statement.
553      */
escapeForLike(@onNull String arg)554     public static String escapeForLike(@NonNull String arg) {
555         final StringBuilder sb = new StringBuilder();
556         for (int i = 0; i < arg.length(); i++) {
557             final char c = arg.charAt(i);
558             switch (c) {
559                 case '%': sb.append('\\');
560                     break;
561                 case '_': sb.append('\\');
562                     break;
563             }
564             sb.append(c);
565         }
566         return sb.toString();
567     }
568 
replaceMatchAnyChar(@onNull String[] arg)569     public static String[] replaceMatchAnyChar(@NonNull String[] arg) {
570         String[] result = arg.clone();
571         for (int i = 0; i < arg.length; i++) {
572             if (result[i] != null) {
573                 result[i] = result[i].replace('*', '%');
574             }
575         }
576         return result;
577     }
578 
parseBoolean(@ullable Object value, boolean def)579     public static boolean parseBoolean(@Nullable Object value, boolean def) {
580         if (value instanceof Boolean) {
581             return (Boolean) value;
582         } else if (value instanceof Number) {
583             return ((Number) value).intValue() != 0;
584         } else if (value instanceof String) {
585             final String stringValue = ((String) value).toLowerCase(Locale.ROOT);
586             return (!"false".equals(stringValue) && !"0".equals(stringValue));
587         } else {
588             return def;
589         }
590     }
591 
getAsBoolean(@onNull Bundle extras, @NonNull String key, boolean def)592     public static boolean getAsBoolean(@NonNull Bundle extras,
593             @NonNull String key, boolean def) {
594         return parseBoolean(extras.get(key), def);
595     }
596 
getAsBoolean(@onNull ContentValues values, @NonNull String key, boolean def)597     public static boolean getAsBoolean(@NonNull ContentValues values,
598             @NonNull String key, boolean def) {
599         return parseBoolean(values.get(key), def);
600     }
601 
getAsLong(@onNull ContentValues values, @NonNull String key, long def)602     public static long getAsLong(@NonNull ContentValues values,
603             @NonNull String key, long def) {
604         final Long value = values.getAsLong(key);
605         return (value != null) ? value : def;
606     }
607 }
608