1 /* 2 * Copyright (C) 2022 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.server.healthconnect.storage.utils; 18 19 import static android.health.connect.HealthDataCategory.ACTIVITY; 20 import static android.health.connect.HealthDataCategory.SLEEP; 21 import static android.health.connect.HealthDataCategory.WELLNESS; 22 import static android.health.connect.datatypes.AggregationType.SUM; 23 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_BASAL_METABOLIC_RATE; 24 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_HYDRATION; 25 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_NUTRITION; 26 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_TOTAL_CALORIES_BURNED; 27 import static android.text.TextUtils.isEmpty; 28 29 import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; 30 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.CLIENT_RECORD_ID_COLUMN_NAME; 31 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME; 32 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.UUID_COLUMN_NAME; 33 34 import static java.util.Objects.requireNonNull; 35 36 import android.annotation.Nullable; 37 import android.content.ContentValues; 38 import android.database.Cursor; 39 import android.database.DatabaseUtils; 40 import android.database.sqlite.SQLiteDatabase; 41 import android.health.connect.HealthDataCategory; 42 import android.health.connect.RecordIdFilter; 43 import android.health.connect.internal.datatypes.InstantRecordInternal; 44 import android.health.connect.internal.datatypes.IntervalRecordInternal; 45 import android.health.connect.internal.datatypes.RecordInternal; 46 import android.health.connect.internal.datatypes.utils.HealthConnectMappings; 47 import android.health.connect.internal.datatypes.utils.RecordTypeRecordCategoryMapper; 48 import android.util.Slog; 49 50 import com.android.internal.annotations.VisibleForTesting; 51 52 import java.nio.ByteBuffer; 53 import java.time.ZoneOffset; 54 import java.util.ArrayList; 55 import java.util.Arrays; 56 import java.util.Collection; 57 import java.util.Collections; 58 import java.util.List; 59 import java.util.UUID; 60 import java.util.stream.Collectors; 61 import java.util.stream.Stream; 62 63 /** 64 * An util class for HC storage 65 * 66 * @hide 67 */ 68 public final class StorageUtils { 69 public static final String TEXT_NOT_NULL = "TEXT NOT NULL"; 70 public static final String TEXT_NOT_NULL_UNIQUE = "TEXT NOT NULL UNIQUE"; 71 public static final String TEXT_NULL = "TEXT"; 72 public static final String INTEGER = "INTEGER"; 73 public static final String INTEGER_UNIQUE = "INTEGER UNIQUE"; 74 public static final String INTEGER_NOT_NULL = "INTEGER NOT NULL"; 75 public static final String REAL = "REAL"; 76 public static final String REAL_NOT_NULL = "REAL NOT NULL"; 77 public static final String PRIMARY_AUTOINCREMENT = "INTEGER PRIMARY KEY AUTOINCREMENT"; 78 public static final String PRIMARY = "INTEGER PRIMARY KEY"; 79 public static final String DELIMITER = ","; 80 public static final String BLOB = "BLOB"; 81 public static final String BLOB_UNIQUE_NULL = "BLOB UNIQUE"; 82 public static final String BLOB_NULL = "BLOB NULL"; 83 public static final String BLOB_UNIQUE_NON_NULL = "BLOB NOT NULL UNIQUE"; 84 public static final String BLOB_NON_NULL = "BLOB NOT NULL"; 85 public static final String SELECT_ALL = "SELECT * FROM "; 86 public static final String SELECT = "SELECT "; 87 public static final String FROM = " FROM "; 88 public static final String DISTINCT = "DISTINCT "; 89 public static final String LIMIT_SIZE = " LIMIT "; 90 public static final int BOOLEAN_FALSE_VALUE = 0; 91 public static final int BOOLEAN_TRUE_VALUE = 1; 92 public static final int UUID_BYTE_SIZE = 16; 93 private static final String TAG = "HealthConnectUtils"; 94 95 // Returns null if fetching any of the fields resulted in an error 96 @Nullable getConflictErrorMessageForRecord( Cursor cursor, ContentValues contentValues)97 public static String getConflictErrorMessageForRecord( 98 Cursor cursor, ContentValues contentValues) { 99 try { 100 return "Updating record with uuid: " 101 + convertBytesToUUID(contentValues.getAsByteArray(UUID_COLUMN_NAME)) 102 + " and client record id: " 103 + contentValues.getAsString(CLIENT_RECORD_ID_COLUMN_NAME) 104 + " conflicts with an existing record with uuid: " 105 + getCursorUUID(cursor, UUID_COLUMN_NAME) 106 + " and client record id: " 107 + getCursorString(cursor, CLIENT_RECORD_ID_COLUMN_NAME); 108 } catch (Exception exception) { 109 Slog.e(TAG, "", exception); 110 return null; 111 } 112 } 113 114 /** 115 * Returns a UUID for the given triple {@code resourceId}, {@code resourceType} and {@code 116 * dataSourceId}. 117 */ generateMedicalResourceUUID( String resourceId, int resourceType, String dataSourceId)118 public static UUID generateMedicalResourceUUID( 119 String resourceId, int resourceType, String dataSourceId) { 120 final byte[] resourceIdBytes = resourceId.getBytes(); 121 final byte[] dataSourceIdBytes = dataSourceId.getBytes(); 122 123 byte[] bytes = 124 ByteBuffer.allocate( 125 resourceIdBytes.length + Integer.BYTES + dataSourceIdBytes.length) 126 .put(resourceIdBytes) 127 .putInt(resourceType) 128 .put(dataSourceIdBytes) 129 .array(); 130 return UUID.nameUUIDFromBytes(bytes); 131 } 132 133 /** 134 * Sets UUID for the given record. If {@link RecordInternal#getClientRecordId()} is null or 135 * empty, then the UUID is randomly generated. Otherwise, the UUID is generated as a combination 136 * of {@link RecordInternal#getPackageName()}, {@link RecordInternal#getClientRecordId()} and 137 * {@link RecordInternal#getRecordType()}. 138 */ addNameBasedUUIDTo(RecordInternal<?> recordInternal)139 public static void addNameBasedUUIDTo(RecordInternal<?> recordInternal) { 140 final String clientRecordId = recordInternal.getClientRecordId(); 141 if (isEmpty(clientRecordId)) { 142 recordInternal.setUuid(UUID.randomUUID()); 143 return; 144 } 145 146 final UUID uuid = 147 getUUID( 148 requireNonNull(recordInternal.getPackageName()), 149 clientRecordId, 150 recordInternal.getRecordType()); 151 recordInternal.setUuid(uuid); 152 } 153 154 /** Updates the uuid using the clientRecordID if the clientRecordId is present. */ updateNameBasedUUIDIfRequired(RecordInternal<?> recordInternal)155 public static void updateNameBasedUUIDIfRequired(RecordInternal<?> recordInternal) { 156 final String clientRecordId = recordInternal.getClientRecordId(); 157 if (isEmpty(clientRecordId)) { 158 // If clientRecordID is absent, use the uuid already set in the input record and 159 // hence no need to modify it. 160 return; 161 } 162 163 final UUID uuid = 164 getUUID( 165 requireNonNull(recordInternal.getPackageName()), 166 clientRecordId, 167 recordInternal.getRecordType()); 168 recordInternal.setUuid(uuid); 169 } 170 171 /** 172 * Returns a UUID for the given {@link RecordIdFilter} and package name. If {@link 173 * RecordIdFilter#getClientRecordId()} is null or empty, then the UUID corresponds to {@link 174 * RecordIdFilter#getId()}. Otherwise, the UUID is generated as a combination of the package 175 * name, {@link RecordIdFilter#getClientRecordId()} and {@link RecordIdFilter#getRecordType()}. 176 */ getUUIDFor(RecordIdFilter recordIdFilter, String packageName)177 public static UUID getUUIDFor(RecordIdFilter recordIdFilter, String packageName) { 178 final String clientRecordId = recordIdFilter.getClientRecordId(); 179 if (isEmpty(clientRecordId)) { 180 return UUID.fromString(recordIdFilter.getId()); 181 } 182 183 return getUUID( 184 packageName, 185 clientRecordId, 186 HealthConnectMappings.getInstance().getRecordType(recordIdFilter.getRecordType())); 187 } 188 addPackageNameTo(RecordInternal<?> recordInternal, String packageName)189 public static void addPackageNameTo(RecordInternal<?> recordInternal, String packageName) { 190 recordInternal.setPackageName(packageName); 191 } 192 193 /** Checks if the value of given column is null */ isNullValue(Cursor cursor, String columnName)194 public static boolean isNullValue(Cursor cursor, String columnName) { 195 return cursor.isNull(cursor.getColumnIndex(columnName)); 196 } 197 getCursorString(Cursor cursor, String columnName)198 public static String getCursorString(Cursor cursor, String columnName) { 199 return cursor.getString(cursor.getColumnIndex(columnName)); 200 } 201 getCursorUUID(Cursor cursor, String columnName)202 public static UUID getCursorUUID(Cursor cursor, String columnName) { 203 return convertBytesToUUID(cursor.getBlob(cursor.getColumnIndex(columnName))); 204 } 205 getCursorInt(Cursor cursor, String columnName)206 public static int getCursorInt(Cursor cursor, String columnName) { 207 return cursor.getInt(cursor.getColumnIndex(columnName)); 208 } 209 210 /** Reads integer and converts to false anything apart from 1. */ getIntegerAndConvertToBoolean(Cursor cursor, String columnName)211 public static boolean getIntegerAndConvertToBoolean(Cursor cursor, String columnName) { 212 String value = cursor.getString(cursor.getColumnIndex(columnName)); 213 if (value == null || value.isEmpty()) { 214 return false; 215 } 216 return Integer.parseInt(value) == BOOLEAN_TRUE_VALUE; 217 } 218 getCursorLong(Cursor cursor, String columnName)219 public static long getCursorLong(Cursor cursor, String columnName) { 220 return cursor.getLong(cursor.getColumnIndex(columnName)); 221 } 222 getCursorDouble(Cursor cursor, String columnName)223 public static double getCursorDouble(Cursor cursor, String columnName) { 224 return cursor.getDouble(cursor.getColumnIndex(columnName)); 225 } 226 getCursorBlob(Cursor cursor, String columnName)227 public static byte[] getCursorBlob(Cursor cursor, String columnName) { 228 return cursor.getBlob(cursor.getColumnIndex(columnName)); 229 } 230 getCursorStringList( Cursor cursor, String columnName, String delimiter)231 public static List<String> getCursorStringList( 232 Cursor cursor, String columnName, String delimiter) { 233 final String values = cursor.getString(cursor.getColumnIndex(columnName)); 234 if (values == null || values.isEmpty()) { 235 return Collections.emptyList(); 236 } 237 238 return Arrays.asList(values.split(delimiter)); 239 } 240 getCursorIntegerList( Cursor cursor, String columnName, String delimiter)241 public static List<Integer> getCursorIntegerList( 242 Cursor cursor, String columnName, String delimiter) { 243 final String stringList = cursor.getString(cursor.getColumnIndex(columnName)); 244 if (stringList == null || stringList.isEmpty()) { 245 return Collections.emptyList(); 246 } 247 248 return Arrays.stream(stringList.split(delimiter)) 249 .mapToInt(Integer::valueOf) 250 .boxed() 251 .toList(); 252 } 253 getCursorLongList(Cursor cursor, String columnName, String delimiter)254 public static List<Long> getCursorLongList(Cursor cursor, String columnName, String delimiter) { 255 final String stringList = cursor.getString(cursor.getColumnIndex(columnName)); 256 if (stringList == null || stringList.isEmpty()) { 257 return Collections.emptyList(); 258 } 259 260 return Arrays.stream(stringList.split(delimiter)).mapToLong(Long::valueOf).boxed().toList(); 261 } 262 flattenIntList(List<Integer> values)263 public static String flattenIntList(List<Integer> values) { 264 return values.stream().map(String::valueOf).collect(Collectors.joining(DELIMITER)); 265 } 266 flattenLongList(List<Long> values)267 public static String flattenLongList(List<Long> values) { 268 return values.stream().map(String::valueOf).collect(Collectors.joining(DELIMITER)); 269 } 270 flattenIntArray(int[] values)271 public static String flattenIntArray(int[] values) { 272 return Arrays.stream(values) 273 .mapToObj(String::valueOf) 274 .collect(Collectors.joining(DELIMITER)); 275 } 276 getMaxPrimaryKeyQuery(String tableName)277 private static String getMaxPrimaryKeyQuery(String tableName) { 278 return "SELECT MAX(" 279 + PRIMARY_COLUMN_NAME 280 + ") as " 281 + PRIMARY_COLUMN_NAME 282 + " FROM " 283 + tableName; 284 } 285 286 /** 287 * Reads ZoneOffset using given cursor. Returns null of column name is not present in the table. 288 */ 289 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression getZoneOffset(Cursor cursor, String startZoneOffsetColumnName)290 public static ZoneOffset getZoneOffset(Cursor cursor, String startZoneOffsetColumnName) { 291 ZoneOffset zoneOffset = null; 292 if (cursor.getColumnIndex(startZoneOffsetColumnName) != -1) { 293 zoneOffset = 294 ZoneOffset.ofTotalSeconds( 295 StorageUtils.getCursorInt(cursor, startZoneOffsetColumnName)); 296 } 297 298 return zoneOffset; 299 } 300 301 /** Encodes record properties participating in deduplication into a byte array. */ 302 @Nullable getDedupeByteBuffer(RecordInternal<?> record)303 public static byte[] getDedupeByteBuffer(RecordInternal<?> record) { 304 if (!isEmpty(record.getClientRecordId())) { 305 return null; // If dedupe by clientRecordId then don't dedupe by hash 306 } 307 308 if (record instanceof InstantRecordInternal<?>) { 309 return getDedupeByteBuffer((InstantRecordInternal<?>) record); 310 } 311 312 if (record instanceof IntervalRecordInternal<?>) { 313 return getDedupeByteBuffer((IntervalRecordInternal<?>) record); 314 } 315 316 throw new IllegalArgumentException("Unexpected record type: " + record); 317 } 318 getDedupeByteBuffer(InstantRecordInternal<?> record)319 private static byte[] getDedupeByteBuffer(InstantRecordInternal<?> record) { 320 return ByteBuffer.allocate(Long.BYTES * 3) 321 .putLong(record.getAppInfoId()) 322 .putLong(record.getDeviceInfoId()) 323 .putLong(record.getTimeInMillis()) 324 .array(); 325 } 326 327 @Nullable getDedupeByteBuffer(IntervalRecordInternal<?> record)328 private static byte[] getDedupeByteBuffer(IntervalRecordInternal<?> record) { 329 final int type = record.getRecordType(); 330 if ((type == RECORD_TYPE_HYDRATION) || (type == RECORD_TYPE_NUTRITION)) { 331 return null; // Some records are exempt from deduplication 332 } 333 334 return ByteBuffer.allocate(Long.BYTES * 4) 335 .putLong(record.getAppInfoId()) 336 .putLong(record.getDeviceInfoId()) 337 .putLong(record.getStartTimeInMillis()) 338 .putLong(record.getEndTimeInMillis()) 339 .array(); 340 } 341 342 /** Returns a UUID for the given package name, client record id and record type id. */ getUUID(String packageName, String clientRecordId, int recordTypeId)343 private static UUID getUUID(String packageName, String clientRecordId, int recordTypeId) { 344 final byte[] packageNameBytes = packageName.getBytes(); 345 final byte[] clientRecordIdBytes = clientRecordId.getBytes(); 346 347 byte[] bytes = 348 ByteBuffer.allocate( 349 packageNameBytes.length 350 + Integer.BYTES 351 + clientRecordIdBytes.length) 352 .put(packageNameBytes) 353 .putInt( 354 InternalHealthConnectMappings.getInstance() 355 .getRecordTypeIdForUuid(recordTypeId)) 356 .put(clientRecordIdBytes) 357 .array(); 358 return UUID.nameUUIDFromBytes(bytes); 359 } 360 361 /** 362 * Returns if priority of apps needs to be considered to compute the aggregate request for the 363 * record type. 364 * 365 * @deprecated use {@link InternalHealthConnectMappings#supportsPriority(int, int)} 366 */ 367 @Deprecated supportsPriority(int recordType, int operationType)368 public static boolean supportsPriority(int recordType, int operationType) { 369 if (operationType != SUM) { 370 return false; 371 } 372 373 @HealthDataCategory.Type 374 int recordCategory = 375 RecordTypeRecordCategoryMapper.getRecordCategoryForRecordType(recordType); 376 return recordCategory == ACTIVITY || recordCategory == SLEEP || recordCategory == WELLNESS; 377 } 378 379 /** 380 * Returns if derivation needs to be done to calculate aggregate. 381 * 382 * @deprecated use {@link InternalHealthConnectMappings#isDerivedType(int)} 383 */ 384 @Deprecated isDerivedType(int recordType)385 public static boolean isDerivedType(int recordType) { 386 return recordType == RECORD_TYPE_BASAL_METABOLIC_RATE 387 || recordType == RECORD_TYPE_TOTAL_CALORIES_BURNED; 388 } 389 convertBytesToUUID(byte[] bytes)390 public static UUID convertBytesToUUID(byte[] bytes) { 391 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 392 long high = byteBuffer.getLong(); 393 long low = byteBuffer.getLong(); 394 return new UUID(high, low); 395 } 396 convertUUIDToBytes(UUID uuid)397 public static byte[] convertUUIDToBytes(UUID uuid) { 398 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[16]); 399 byteBuffer.putLong(uuid.getMostSignificantBits()); 400 byteBuffer.putLong(uuid.getLeastSignificantBits()); 401 return byteBuffer.array(); 402 } 403 404 /** Convert a double value to bytes. */ convertDoubleToBytes(double value)405 public static byte[] convertDoubleToBytes(double value) { 406 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]); 407 byteBuffer.putDouble(value); 408 return byteBuffer.array(); 409 } 410 411 /** Convert bytes to a double. */ convertBytesToDouble(byte[] bytes)412 public static double convertBytesToDouble(byte[] bytes) { 413 return ByteBuffer.wrap(bytes).getDouble(); 414 } 415 416 /** Convert an integer value to bytes. */ convertIntToBytes(int value)417 public static byte[] convertIntToBytes(int value) { 418 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[4]); 419 byteBuffer.putInt(value); 420 return byteBuffer.array(); 421 } 422 423 /** Convert bytes to an integer. */ convertBytesToInt(byte[] bytes)424 public static int convertBytesToInt(byte[] bytes) { 425 return ByteBuffer.wrap(bytes).getInt(); 426 } 427 428 /** Convert bytes to a long. */ convertLongToBytes(long value)429 public static byte[] convertLongToBytes(long value) { 430 ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[8]); 431 byteBuffer.putLong(value); 432 return byteBuffer.array(); 433 } 434 435 /** Convert a long value to bytes. */ convertBytesToLong(byte[] bytes)436 public static long convertBytesToLong(byte[] bytes) { 437 return ByteBuffer.wrap(bytes).getLong(); 438 } 439 440 /** 441 * Creates a list of UUIDs from a collection of the string representation of the UUIDs. Any ids 442 * which cannot be parsed as UUIDs are ignores. It is the responsibility of the caller to handle 443 * the case where a non-empty list becomes empty. 444 * 445 * @param ids the ids to parse 446 * @return a possibly empty list of UUIDs 447 */ toUuids(Collection<String> ids)448 public static List<UUID> toUuids(Collection<String> ids) { 449 return ids.stream() 450 .flatMap( 451 id -> { 452 try { 453 return Stream.of(UUID.fromString(id)); 454 } catch (IllegalArgumentException ex) { 455 return Stream.of(); 456 } 457 }) 458 .toList(); 459 } 460 461 /** Converts a list of {@link UUID} strings to a list of hex strings. */ 462 public static List<String> convertUuidStringsToHexStrings(List<String> ids) { 463 return StorageUtils.getListOfHexStrings(toUuids(ids)); 464 } 465 466 public static String getHexString(byte[] value) { 467 if (value == null) { 468 return ""; 469 } 470 471 final StringBuilder builder = new StringBuilder("x'"); 472 for (byte b : value) { 473 builder.append(String.format("%02x", b)); 474 } 475 builder.append("'"); 476 477 return builder.toString(); 478 } 479 480 public static String getHexString(UUID uuid) { 481 return getHexString(convertUUIDToBytes(uuid)); 482 } 483 484 /** Creates a list of Hex strings for a given list of {@code UUID}s. */ 485 public static List<String> getListOfHexStrings(List<UUID> uuids) { 486 List<String> hexStrings = new ArrayList<>(); 487 for (UUID uuid : uuids) { 488 hexStrings.add(getHexString(convertUUIDToBytes(uuid))); 489 } 490 491 return hexStrings; 492 } 493 494 /** 495 * Returns a byte array containing sublist of the given uuids list, from position {@code 496 * start}(inclusive) to {@code end}(exclusive). 497 */ 498 public static byte[] getSingleByteArray(List<UUID> uuids) { 499 byte[] allByteArray = new byte[UUID_BYTE_SIZE * uuids.size()]; 500 501 ByteBuffer byteBuffer = ByteBuffer.wrap(allByteArray); 502 for (UUID uuid : uuids) { 503 byteBuffer.put(convertUUIDToBytes(uuid)); 504 } 505 506 return byteBuffer.array(); 507 } 508 509 public static List<UUID> getCursorUUIDList(Cursor cursor, String columnName) { 510 byte[] bytes = cursor.getBlob(cursor.getColumnIndex(columnName)); 511 return bytesToUuids(bytes); 512 } 513 514 /** Turns a byte array to a UUID list. */ 515 @VisibleForTesting(visibility = PRIVATE) 516 public static List<UUID> bytesToUuids(byte[] bytes) { 517 ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); 518 519 List<UUID> uuidList = new ArrayList<>(); 520 while (byteBuffer.hasRemaining()) { 521 long high = byteBuffer.getLong(); 522 long low = byteBuffer.getLong(); 523 uuidList.add(new UUID(high, low)); 524 } 525 return uuidList; 526 } 527 528 /** 529 * Returns a quoted, escaped id if {@code id} is not quoted. Following examples show the 530 * expected return values, 531 * 532 * <p>getNormalisedId("id") -> "'id'" 533 * 534 * <p>getNormalisedId("'id'") -> "'id'" 535 * 536 * <p>getNormalisedId("x'id'") -> "x'id'" 537 */ 538 public static String getNormalisedString(String id) { 539 StringBuilder result = new StringBuilder(); 540 541 String innerId; 542 if (id.startsWith("'") && id.endsWith("'")) { 543 innerId = id.substring(1, id.length() - 1); 544 } else if ((id.startsWith("x'") || id.startsWith("X'")) && id.endsWith("'")) { 545 innerId = id.substring(2, id.length() - 1); 546 result.append(id.charAt(0)); 547 } else { 548 innerId = id; 549 } 550 551 DatabaseUtils.appendEscapedSQLString(result, innerId); 552 553 return result.toString(); 554 } 555 556 /** Checks whether {@code tableName} exists in the {@code database}. */ 557 public static boolean checkTableExists(SQLiteDatabase database, String tableName) { 558 try (Cursor cursor = 559 database.rawQuery( 560 "SELECT name FROM sqlite_master WHERE type='table' AND name=?", 561 new String[] {tableName})) { 562 if (cursor.getCount() == 0) { 563 Slog.d(TAG, "Table does not exist: " + tableName); 564 } 565 return cursor.getCount() > 0; 566 } 567 } 568 569 /** Gets the last id for {@code tableName} exists in the {@code database}. */ 570 public static long getLastRowIdFor(SQLiteDatabase database, String tableName) { 571 try (Cursor cursor = 572 database.rawQuery(StorageUtils.getMaxPrimaryKeyQuery(tableName), null)) { 573 cursor.moveToFirst(); 574 return cursor.getLong(cursor.getColumnIndex(PRIMARY_COLUMN_NAME)); 575 } 576 } 577 578 /** Extracts and holds data from {@link ContentValues}. */ 579 public static class RecordIdentifierData { 580 private final String mClientRecordId; 581 private final UUID mUuid; 582 583 public RecordIdentifierData(ContentValues contentValues) { 584 mClientRecordId = contentValues.getAsString(CLIENT_RECORD_ID_COLUMN_NAME); 585 mUuid = StorageUtils.convertBytesToUUID(contentValues.getAsByteArray(UUID_COLUMN_NAME)); 586 } 587 588 @Nullable 589 public String getClientRecordId() { 590 return mClientRecordId; 591 } 592 593 @Nullable 594 public UUID getUuid() { 595 return mUuid; 596 } 597 598 @Override 599 public String toString() { 600 final StringBuilder builder = new StringBuilder(); 601 if (mClientRecordId != null && !mClientRecordId.isEmpty()) { 602 builder.append("clientRecordID : ").append(mClientRecordId).append(" , "); 603 } 604 605 if (mUuid != null) { 606 builder.append("uuid : ").append(mUuid).append(" , "); 607 } 608 return builder.toString(); 609 } 610 } 611 } 612