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