1 /* 2 * Copyright (C) 2024 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.exportimport; 18 19 import static android.health.connect.Constants.DEFAULT_LONG; 20 import static android.health.connect.Constants.DEFAULT_PAGE_SIZE; 21 import static android.health.connect.PageTokenWrapper.EMPTY_PAGE_TOKEN; 22 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_EXERCISE_SESSION; 23 import static android.health.connect.datatypes.RecordTypeIdentifier.RECORD_TYPE_PLANNED_EXERCISE_SESSION; 24 25 import static com.android.healthfitness.flags.AconfigFlagHelper.isCloudBackupRestoreEnabled; 26 import static com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper.APP_ID_PRIORITY_ORDER_COLUMN_NAME; 27 import static com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper.HEALTH_DATA_CATEGORY_COLUMN_NAME; 28 import static com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper.PRIORITY_TABLE_NAME; 29 import static com.android.server.healthconnect.storage.datatypehelpers.MedicalDataSourceHelper.getReadQueryForDataSourcesUsingUniqueIds; 30 import static com.android.server.healthconnect.storage.utils.StorageUtils.DELIMITER; 31 import static com.android.server.healthconnect.storage.utils.StorageUtils.checkTableExists; 32 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong; 33 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString; 34 35 import static java.util.Objects.requireNonNull; 36 37 import android.content.ContentValues; 38 import android.database.Cursor; 39 import android.database.sqlite.SQLiteDatabase; 40 import android.health.connect.PageTokenWrapper; 41 import android.health.connect.ReadRecordsRequestUsingFilters; 42 import android.health.connect.datatypes.MedicalDataSource; 43 import android.health.connect.datatypes.MedicalResource; 44 import android.health.connect.datatypes.Record; 45 import android.health.connect.internal.datatypes.PlannedExerciseSessionRecordInternal; 46 import android.health.connect.internal.datatypes.RecordInternal; 47 import android.health.connect.internal.datatypes.utils.HealthConnectMappings; 48 import android.util.ArrayMap; 49 import android.util.Pair; 50 import android.util.Slog; 51 52 import com.android.healthfitness.flags.Flags; 53 import com.android.server.healthconnect.fitness.FitnessRecordReadHelper; 54 import com.android.server.healthconnect.fitness.FitnessRecordUpsertHelper; 55 import com.android.server.healthconnect.phr.PhrPageTokenWrapper; 56 import com.android.server.healthconnect.phr.ReadMedicalResourcesInternalResponse; 57 import com.android.server.healthconnect.storage.HealthConnectDatabase; 58 import com.android.server.healthconnect.storage.TransactionManager; 59 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper; 60 import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsRequestHelper; 61 import com.android.server.healthconnect.storage.datatypehelpers.DeviceInfoHelper; 62 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper; 63 import com.android.server.healthconnect.storage.datatypehelpers.MedicalDataSourceHelper; 64 import com.android.server.healthconnect.storage.datatypehelpers.MedicalResourceHelper; 65 import com.android.server.healthconnect.storage.datatypehelpers.MedicalResourceIndicesHelper; 66 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper; 67 import com.android.server.healthconnect.storage.request.DeleteTableRequest; 68 import com.android.server.healthconnect.storage.request.ReadTableRequest; 69 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings; 70 import com.android.server.healthconnect.storage.utils.StorageUtils; 71 72 import java.util.ArrayList; 73 import java.util.HashMap; 74 import java.util.List; 75 import java.util.Map; 76 import java.util.stream.Stream; 77 78 /** 79 * Merges a secondary database's contents with the HC database. This will be used in D2D migration 80 * and Export/Import. 81 * 82 * @hide 83 */ 84 public final class DatabaseMerger { 85 86 private static final String TAG = "HealthConnectDatabaseMerger"; 87 88 private final TransactionManager mTransactionManager; 89 private final FitnessRecordUpsertHelper mFitnessRecordUpsertHelper; 90 private final FitnessRecordReadHelper mFitnessRecordReadHelper; 91 private final AppInfoHelper mAppInfoHelper; 92 private final HealthConnectMappings mHealthConnectMappings; 93 private final InternalHealthConnectMappings mInternalHealthConnectMappings; 94 private final HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper; 95 96 /* 97 * Record types in this list will always be migrated such that the ordering here is respected. 98 * When adding a new priority override, group the types that need to migrated together within 99 * their own list. This makes the logical separation clear and also reduces storage usage during 100 * migration, as we delete the original records. 101 */ 102 public static final List<List<Integer>> RECORD_TYPE_MIGRATION_ORDERING_OVERRIDES = 103 List.of( 104 // Training plans must be migrated before exercise sessions. Exercise sessions 105 // may contain a reference to a training plan, so the training plan needs to 106 // exist so that the foreign key constraints are not violated. 107 List.of(RECORD_TYPE_PLANNED_EXERCISE_SESSION, RECORD_TYPE_EXERCISE_SESSION)); 108 109 private static final List<String> PHR_TABLES_TO_MERGE = 110 List.of( 111 MedicalDataSourceHelper.getMainTableName(), 112 MedicalResourceHelper.getMainTableName(), 113 MedicalResourceIndicesHelper.getTableName()); 114 DatabaseMerger( AppInfoHelper appInfoHelper, DeviceInfoHelper deviceInfoHelper, HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, TransactionManager transactionManager, FitnessRecordUpsertHelper fitnessRecordUpsertHelper, FitnessRecordReadHelper fitnessRecordReadHelper)115 public DatabaseMerger( 116 AppInfoHelper appInfoHelper, 117 DeviceInfoHelper deviceInfoHelper, 118 HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, 119 TransactionManager transactionManager, 120 FitnessRecordUpsertHelper fitnessRecordUpsertHelper, 121 FitnessRecordReadHelper fitnessRecordReadHelper) { 122 mTransactionManager = transactionManager; 123 mFitnessRecordUpsertHelper = fitnessRecordUpsertHelper; 124 mFitnessRecordReadHelper = fitnessRecordReadHelper; 125 mAppInfoHelper = appInfoHelper; 126 mHealthConnectMappings = HealthConnectMappings.getInstance(); 127 mInternalHealthConnectMappings = InternalHealthConnectMappings.getInstance(); 128 mHealthDataCategoryPriorityHelper = healthDataCategoryPriorityHelper; 129 } 130 131 /** Merge data */ 132 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression merge(HealthConnectDatabase stagedDatabase)133 public synchronized void merge(HealthConnectDatabase stagedDatabase) { 134 TransactionManager stagedTransactionManager = 135 TransactionManager.forStagedDatabase( 136 stagedDatabase, mInternalHealthConnectMappings); 137 138 Slog.i(TAG, "Merging app info"); 139 140 Map<Long, String> stagedPackageNamesByAppIds = new ArrayMap<>(); 141 try (Cursor cursor = read(stagedDatabase, new ReadTableRequest(AppInfoHelper.TABLE_NAME))) { 142 while (cursor.moveToNext()) { 143 long rowId = getCursorLong(cursor, RecordHelper.PRIMARY_COLUMN_NAME); 144 String packageName = getCursorString(cursor, AppInfoHelper.PACKAGE_COLUMN_NAME); 145 String appName = getCursorString(cursor, AppInfoHelper.APPLICATION_COLUMN_NAME); 146 stagedPackageNamesByAppIds.put(rowId, packageName); 147 148 // If this package is not installed on the target device and is not present in the 149 // health db, then fill the health db with the info from source db. According to the 150 // security review b/341253579, we should not parse the imported icon. 151 mAppInfoHelper.addAppInfoIfNoAppInfoEntryExists(packageName, appName); 152 } 153 } 154 155 // Similar to current HC behaviour, we honour what is on the target device. This means 156 // that if a MedicalResource or MedicalDataSource of the same unique ids as the 157 // stagedDatabase exists on the targetDatabase, we ignore the one in stagedDatabase. 158 // TODO(b/376645901): Verify that there's no timeout with large datasets on actual 159 // devices. 160 if (Flags.personalHealthRecordEnableD2dAndExportImport()) { 161 Slog.i(TAG, "Merging PHR data"); 162 try { 163 mergePhrContent(stagedDatabase.getReadableDatabase()); 164 } catch (Exception e) { 165 Slog.e(TAG, "Failed to transfer PHR data from staged database", e); 166 } 167 } 168 169 Slog.i(TAG, "Merging records"); 170 171 // Determine the order in which we should migrate data types. This involves first 172 // migrating data types according to the specified ordering overrides. Remaining 173 // records are migrated in no particular order. 174 List<Integer> recordTypesWithOrderingOverrides = 175 RECORD_TYPE_MIGRATION_ORDERING_OVERRIDES.stream().flatMap(List::stream).toList(); 176 List<Integer> recordTypesWithoutOrderingOverrides = 177 mHealthConnectMappings.getRecordIdToExternalRecordClassMap().keySet().stream() 178 .filter(it -> !recordTypesWithOrderingOverrides.contains(it)) 179 .toList(); 180 181 // Migrate special case records in their defined order. 182 for (List<Integer> recordTypeMigrationGroup : RECORD_TYPE_MIGRATION_ORDERING_OVERRIDES) { 183 for (int recordTypeToMigrate : recordTypeMigrationGroup) { 184 mergeRecordsOfType( 185 stagedTransactionManager, 186 stagedDatabase, 187 stagedPackageNamesByAppIds, 188 recordTypeToMigrate); 189 } 190 // Delete records within a group together, once all records within that group 191 // have been migrated. This ensures referential integrity is preserved during 192 // migration. 193 for (int recordTypeToMigrate : recordTypeMigrationGroup) { 194 deleteRecordsOfType(stagedDatabase, recordTypeToMigrate); 195 } 196 } 197 // Migrate remaining record types in no particular order. 198 for (Integer recordTypeToMigrate : recordTypesWithoutOrderingOverrides) { 199 mergeRecordsOfType( 200 stagedTransactionManager, 201 stagedDatabase, 202 stagedPackageNamesByAppIds, 203 recordTypeToMigrate); 204 deleteRecordsOfType(stagedDatabase, recordTypeToMigrate); 205 } 206 207 Slog.i(TAG, "Syncing app info records after restored data merge"); 208 mAppInfoHelper.syncAppInfoRecordTypesUsed(); 209 210 Slog.i(TAG, "Merging priority list"); 211 mergePriorityList(stagedDatabase, stagedPackageNamesByAppIds); 212 213 Slog.i(TAG, "Merging done"); 214 } 215 mergePhrContent(SQLiteDatabase stagedDatabase)216 private void mergePhrContent(SQLiteDatabase stagedDatabase) { 217 if (!checkPhrTablesExist(stagedDatabase)) { 218 return; 219 } 220 // We have made the decision to not transfer partial PHR data to the target device. 221 // Hence why we wrap it in a transaction to ensure either all or none of the PHR 222 // data is transferred to the target device. 223 mTransactionManager.runAsTransaction( 224 targetDatabase -> { 225 Map<String, Long> dataSourceUuidToRowId = 226 mergeMedicalDataSourceTable(stagedDatabase, targetDatabase); 227 mergeMedicalResourceAndIndices( 228 stagedDatabase, targetDatabase, dataSourceUuidToRowId); 229 }); 230 } 231 checkPhrTablesExist(SQLiteDatabase stagedDatabase)232 private boolean checkPhrTablesExist(SQLiteDatabase stagedDatabase) { 233 for (String table : PHR_TABLES_TO_MERGE) { 234 if (!checkTableExists(stagedDatabase, table)) { 235 return false; 236 } 237 } 238 return true; 239 } 240 mergeMedicalDataSourceTable( SQLiteDatabase stagedDatabase, SQLiteDatabase targetDatabase)241 private Map<String, Long> mergeMedicalDataSourceTable( 242 SQLiteDatabase stagedDatabase, SQLiteDatabase targetDatabase) { 243 // Read the dataSources from the staged database along with their lastModifiedTimestamp. 244 // We don't want to update the lastModifiedTimestamp, as this currently holds a different 245 // meaning in PHR. We use the lastModifiedTimestamp columns in MedicalResource and 246 // MedicalDataSource to understand when an app has updated the MedicalResource/DataSource. 247 // Since the merge process is not the source app writing the data, we write the 248 // lastModifiedTimestamp using what is in the stagedDatabase rather than based on the 249 // current merge time. 250 List<Pair<MedicalDataSource, Long>> dataSourceTimestampPairs = 251 readMedicalDataSources(stagedDatabase); 252 // To map dataSource uuid string to its rowId in the targetDatabase. 253 Map<String, Long> uuidToRowId = new ArrayMap<>(); 254 for (Pair<MedicalDataSource, Long> dataSourceAndTimestamp : dataSourceTimestampPairs) { 255 MedicalDataSource dataSource = dataSourceAndTimestamp.first; 256 long lastModifiedTime = dataSourceAndTimestamp.second; 257 // Get the appId from the target database. 258 long appInfoId = mAppInfoHelper.getAppInfoId(dataSource.getPackageName()); 259 if (appInfoId == DEFAULT_LONG) { 260 throw new IllegalStateException("App id does not exist."); 261 } 262 263 long insertedRowId = 264 targetDatabase.insertWithOnConflict( 265 MedicalDataSourceHelper.getMainTableName(), 266 /* nullColumnHack= */ null, 267 MedicalDataSourceHelper.getContentValues( 268 dataSource, appInfoId, lastModifiedTime), 269 SQLiteDatabase.CONFLICT_IGNORE); 270 271 // If insertedRowId is -1, there probably was a conflict. In this case, we need to do 272 // a read on the targetDatabase, to find out the rowId of the existing dataSource 273 // with the same unique ids as the one we were trying to insert. 274 if (insertedRowId == DEFAULT_LONG) { 275 insertedRowId = 276 readMedicalDataSourcesUsingDisplayNameAndAppId( 277 targetDatabase, dataSource.getDisplayName(), appInfoId); 278 } 279 280 uuidToRowId.put(dataSource.getId(), insertedRowId); 281 } 282 283 return uuidToRowId; 284 } 285 mergeMedicalResourceAndIndices( SQLiteDatabase stagedDatabase, SQLiteDatabase targetDatabase, Map<String, Long> uuidToRowId)286 private void mergeMedicalResourceAndIndices( 287 SQLiteDatabase stagedDatabase, 288 SQLiteDatabase targetDatabase, 289 Map<String, Long> uuidToRowId) { 290 String nextPageToken = null; 291 do { 292 // Read MedicalResources from staged database. 293 ReadMedicalResourcesInternalResponse response = 294 readMedicalResources( 295 stagedDatabase, 296 PhrPageTokenWrapper.fromPageTokenAllowingNull(nextPageToken)); 297 298 // Write MedicalResources to the target database. 299 for (MedicalResource medicalResource : response.getMedicalResources()) { 300 String dataSourceUuid = medicalResource.getDataSourceId(); 301 Long dataSourceRowId = uuidToRowId.get(dataSourceUuid); 302 if (dataSourceRowId == null) { 303 throw new IllegalStateException("DataSource UUID was not found"); 304 } 305 306 ContentValues contentValues = 307 MedicalResourceHelper.getContentValues( 308 dataSourceRowId, 309 medicalResource.getLastModifiedTimestamp(), 310 medicalResource); 311 long medicalResourceRowId = 312 targetDatabase.insertWithOnConflict( 313 MedicalResourceHelper.getMainTableName(), 314 /* nullColumnHack= */ null, 315 contentValues, 316 SQLiteDatabase.CONFLICT_IGNORE); 317 318 // With CONFLICT_IGNORE, if there already exists a row with the same unique ids 319 // the insertion would be ignored and -1 is returned. In this case, we would 320 // want to continue with copying the rest of the data. 321 if (medicalResourceRowId != DEFAULT_LONG) { 322 targetDatabase.insertWithOnConflict( 323 MedicalResourceIndicesHelper.getTableName(), 324 /* nullColumnHack= */ null, 325 MedicalResourceIndicesHelper.getContentValues( 326 medicalResourceRowId, medicalResource.getType()), 327 SQLiteDatabase.CONFLICT_IGNORE); 328 } 329 } 330 331 nextPageToken = response.getPageToken(); 332 333 } while (nextPageToken != null); 334 } 335 readMedicalDataSources( SQLiteDatabase stagedDatabase)336 private List<Pair<MedicalDataSource, Long>> readMedicalDataSources( 337 SQLiteDatabase stagedDatabase) { 338 try (Cursor cursor = 339 read(stagedDatabase, MedicalDataSourceHelper.getReadQueryForDataSources())) { 340 return MedicalDataSourceHelper.getMedicalDataSourcesWithTimestamps(cursor); 341 } 342 } 343 readMedicalDataSourcesUsingDisplayNameAndAppId( SQLiteDatabase targetDatabase, String displayName, long appId)344 private long readMedicalDataSourcesUsingDisplayNameAndAppId( 345 SQLiteDatabase targetDatabase, String displayName, long appId) { 346 try (Cursor cursor = 347 mTransactionManager.read( 348 targetDatabase, 349 getReadQueryForDataSourcesUsingUniqueIds(displayName, appId))) { 350 return MedicalDataSourceHelper.readDisplayNameAndAppIdFromCursor(cursor); 351 } 352 } 353 readMedicalResources( SQLiteDatabase stagedDatabase, PhrPageTokenWrapper pageTokenWrapper)354 private ReadMedicalResourcesInternalResponse readMedicalResources( 355 SQLiteDatabase stagedDatabase, PhrPageTokenWrapper pageTokenWrapper) { 356 ReadTableRequest readTableRequest = 357 MedicalResourceHelper.getReadTableRequestUsingRequestFilters( 358 pageTokenWrapper, DEFAULT_PAGE_SIZE); 359 return MedicalResourceHelper.getMedicalResources( 360 stagedDatabase, readTableRequest, pageTokenWrapper, DEFAULT_PAGE_SIZE); 361 } 362 mergePriorityList( HealthConnectDatabase stagedDatabase, Map<Long, String> importedAppInfo)363 private void mergePriorityList( 364 HealthConnectDatabase stagedDatabase, Map<Long, String> importedAppInfo) { 365 Map<Integer, List<String>> importPriorityMap = new HashMap<>(); 366 try (Cursor cursor = read(stagedDatabase, new ReadTableRequest(PRIORITY_TABLE_NAME))) { 367 while (cursor.moveToNext()) { 368 int dataCategory = 369 cursor.getInt( 370 cursor.getColumnIndexOrThrow(HEALTH_DATA_CATEGORY_COLUMN_NAME)); 371 List<Long> appIdsInOrder = 372 StorageUtils.getCursorLongList( 373 cursor, APP_ID_PRIORITY_ORDER_COLUMN_NAME, DELIMITER); 374 Slog.i(TAG, "Priority count for " + dataCategory + ": " + appIdsInOrder.size()); 375 importPriorityMap.put( 376 dataCategory, getPackageNamesFromImport(appIdsInOrder, importedAppInfo)); 377 } 378 } 379 380 importPriorityMap.forEach( 381 (category, importPriorityList) -> { 382 if (importPriorityList.isEmpty()) { 383 return; 384 } 385 386 List<String> currentPriorityList = 387 mAppInfoHelper.getPackageNames( 388 mHealthDataCategoryPriorityHelper.getAppIdPriorityOrder( 389 category)); 390 List<String> newPriorityList = 391 Stream.concat(currentPriorityList.stream(), importPriorityList.stream()) 392 .distinct() 393 .toList(); 394 mHealthDataCategoryPriorityHelper.setPriorityOrder(category, newPriorityList); 395 Slog.d( 396 TAG, 397 "Added " 398 + importPriorityList.size() 399 + " apps to priority list of category " 400 + category); 401 }); 402 } 403 mergeRecordsOfType( TransactionManager stagedTransactionManager, HealthConnectDatabase stagedDatabase, Map<Long, String> stagedPackageNamesByAppIds, int recordType)404 private void mergeRecordsOfType( 405 TransactionManager stagedTransactionManager, 406 HealthConnectDatabase stagedDatabase, 407 Map<Long, String> stagedPackageNamesByAppIds, 408 int recordType) { 409 RecordHelper<?> recordHelper = mInternalHealthConnectMappings.getRecordHelper(recordType); 410 if (!checkTableExists( 411 stagedDatabase.getReadableDatabase(), recordHelper.getMainTableName())) { 412 return; 413 } 414 415 Class<? extends Record> recordTypeClass = 416 mHealthConnectMappings.getRecordIdToExternalRecordClassMap().get(recordType); 417 // Read all the records of the given type from the staged db and insert them into the 418 // existing healthconnect db. 419 PageTokenWrapper currentToken = EMPTY_PAGE_TOKEN; 420 do { 421 var recordsToMergeAndToken = 422 getRecordsToMerge( 423 stagedTransactionManager, 424 stagedPackageNamesByAppIds, 425 requireNonNull(recordTypeClass), 426 currentToken); 427 List<RecordInternal<?>> records = recordsToMergeAndToken.first; 428 PageTokenWrapper token = recordsToMergeAndToken.second; 429 if (records.isEmpty()) { 430 Slog.d(TAG, "No records to merge: " + recordTypeClass); 431 break; 432 } 433 Slog.d(TAG, "Found records to merge: " + recordTypeClass); 434 if (recordType == RECORD_TYPE_PLANNED_EXERCISE_SESSION) { 435 // For training plans we nullify any autogenerated references to exercise sessions. 436 // When the corresponding exercise sessions get migrated, these references will be 437 // automatically generated again. 438 records.forEach( 439 it -> { 440 PlannedExerciseSessionRecordInternal record = 441 (PlannedExerciseSessionRecordInternal) it; 442 record.setCompletedExerciseSessionId(null); 443 }); 444 } 445 446 // Both methods use ON CONFLICT IGNORE strategy, which means that if the source data 447 // being inserted into target db already exists, the source data will be ignored. We 448 // won't apply updates to the target data. 449 // 450 // Only generate change logs when any change logs token are present. Client apps can 451 // only read change logs if they have ever requested a change logs token. 452 if (isCloudBackupRestoreEnabled() 453 && mTransactionManager.checkTableExists(ChangeLogsRequestHelper.TABLE_NAME) 454 && mTransactionManager.queryNumEntries(ChangeLogsRequestHelper.TABLE_NAME) 455 != 0) { 456 mFitnessRecordUpsertHelper.insertRecordsUnrestricted( 457 records, /* shouldGenerateChangeLog= */ true); 458 459 } else { 460 mFitnessRecordUpsertHelper.insertRecordsUnrestricted( 461 records, /* shouldGenerateChangeLog= */ false); 462 } 463 currentToken = token; 464 } while (!currentToken.isEmpty()); 465 } 466 deleteRecordsOfType(HealthConnectDatabase stagedDatabase, int recordType)467 private void deleteRecordsOfType(HealthConnectDatabase stagedDatabase, int recordType) { 468 RecordHelper<?> recordHelper = mInternalHealthConnectMappings.getRecordHelper(recordType); 469 if (!checkTableExists( 470 stagedDatabase.getReadableDatabase(), recordHelper.getMainTableName())) { 471 return; 472 } 473 474 // Passing -1 for startTime and endTime as we don't want to have time based filtering in the 475 // final query. 476 Class<? extends Record> recordTypeClass = 477 mHealthConnectMappings.getRecordIdToExternalRecordClassMap().get(recordType); 478 Slog.d(TAG, "Deleting table for: " + recordTypeClass); 479 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression 480 DeleteTableRequest deleteTableRequest = 481 recordHelper.getDeleteTableRequest( 482 null /* packageFilters */, 483 DEFAULT_LONG /* startTime */, 484 DEFAULT_LONG /* endTime */, 485 false /* useLocalTimeFilter */, 486 mAppInfoHelper); 487 488 stagedDatabase.getWritableDatabase().execSQL(deleteTableRequest.getDeleteCommand()); 489 } 490 getRecordsToMerge( TransactionManager stagedTransactionManager, Map<Long, String> stagedPackageNamesByAppIds, Class<? extends Record> recordTypeClass, PageTokenWrapper requestToken)491 private Pair<List<RecordInternal<?>>, PageTokenWrapper> getRecordsToMerge( 492 TransactionManager stagedTransactionManager, 493 Map<Long, String> stagedPackageNamesByAppIds, 494 Class<? extends Record> recordTypeClass, 495 PageTokenWrapper requestToken) { 496 ReadRecordsRequestUsingFilters<?> readRecordsRequest = 497 new ReadRecordsRequestUsingFilters.Builder<>(recordTypeClass) 498 .setPageSize(DEFAULT_PAGE_SIZE) 499 .setPageToken(requestToken.encode()) 500 .build(); 501 502 return mFitnessRecordReadHelper.readRecordsUnrestricted( 503 stagedTransactionManager, 504 readRecordsRequest.toReadRecordsRequestParcel(), 505 stagedPackageNamesByAppIds); 506 } 507 read( HealthConnectDatabase stagedDatabase, ReadTableRequest request)508 private synchronized Cursor read( 509 HealthConnectDatabase stagedDatabase, ReadTableRequest request) { 510 return read(stagedDatabase.getReadableDatabase(), request.getReadCommand()); 511 } 512 read(SQLiteDatabase stagedDatabase, String query)513 private synchronized Cursor read(SQLiteDatabase stagedDatabase, String query) { 514 Slog.d(TAG, "Running command: " + query); 515 Cursor cursor = stagedDatabase.rawQuery(query, null); 516 Slog.d(TAG, "Cursor count: " + cursor.getCount()); 517 return cursor; 518 } 519 520 /** 521 * Returns a list of package names, mapped from the passed-in {@code packageIds} list using the 522 * mapping from the import file. 523 */ getPackageNamesFromImport( List<Long> packageIds, Map<Long, String> importedPackageNameMapping)524 private static List<String> getPackageNamesFromImport( 525 List<Long> packageIds, Map<Long, String> importedPackageNameMapping) { 526 List<String> packageNames = new ArrayList<>(); 527 if (packageIds == null || packageIds.isEmpty() || importedPackageNameMapping.isEmpty()) { 528 return packageNames; 529 } 530 packageIds.forEach( 531 (packageId) -> { 532 String packageName = importedPackageNameMapping.get(packageId); 533 requireNonNull(packageName); 534 packageNames.add(packageName); 535 }); 536 return packageNames; 537 } 538 } 539