• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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