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