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