1 /* 2 * Copyright (C) 2023 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.migration; 18 19 import android.annotation.Nullable; 20 import android.content.Context; 21 import android.database.sqlite.SQLiteDatabase; 22 import android.health.connect.internal.datatypes.RecordInternal; 23 import android.health.connect.migration.AppInfoMigrationPayload; 24 import android.health.connect.migration.MetadataMigrationPayload; 25 import android.health.connect.migration.MigrationEntity; 26 import android.health.connect.migration.MigrationPayload; 27 import android.health.connect.migration.PermissionMigrationPayload; 28 import android.health.connect.migration.PriorityMigrationPayload; 29 import android.health.connect.migration.RecordMigrationPayload; 30 import android.os.UserHandle; 31 32 import com.android.internal.annotations.GuardedBy; 33 import com.android.server.healthconnect.permission.FirstGrantTimeManager; 34 import com.android.server.healthconnect.permission.HealthConnectPermissionHelper; 35 import com.android.server.healthconnect.storage.TransactionManager; 36 import com.android.server.healthconnect.storage.datatypehelpers.ActivityDateHelper; 37 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper; 38 import com.android.server.healthconnect.storage.datatypehelpers.DeviceInfoHelper; 39 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper; 40 import com.android.server.healthconnect.storage.datatypehelpers.MigrationEntityHelper; 41 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 42 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings; 43 import com.android.server.healthconnect.storage.utils.PreferencesManager; 44 import com.android.server.healthconnect.storage.utils.StorageUtils; 45 46 import java.util.ArrayList; 47 import java.util.Collection; 48 import java.util.List; 49 import java.util.stream.Collectors; 50 import java.util.stream.Stream; 51 52 /** 53 * Controls the data migration flow. Accepts and applies collections of {@link MigrationEntity}. 54 * 55 * @hide 56 */ 57 public final class DataMigrationManager { 58 59 private static final Object sLock = new Object(); 60 61 private final Context mUserContext; 62 private final TransactionManager mTransactionManager; 63 private final HealthConnectPermissionHelper mPermissionHelper; 64 private final FirstGrantTimeManager mFirstGrantTimeManager; 65 private final DeviceInfoHelper mDeviceInfoHelper; 66 private final AppInfoHelper mAppInfoHelper; 67 private final PriorityMigrationHelper mPriorityMigrationHelper; 68 private final HealthDataCategoryPriorityHelper mHealthDataCategoryPriorityHelper; 69 private final MigrationEntityHelper mMigrationEntityHelper; 70 private final PreferencesManager mPreferencesManager; 71 DataMigrationManager( Context userContext, TransactionManager transactionManager, HealthConnectPermissionHelper permissionHelper, FirstGrantTimeManager firstGrantTimeManager, DeviceInfoHelper deviceInfoHelper, AppInfoHelper appInfoHelper, HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, PriorityMigrationHelper priorityMigrationHelper, MigrationEntityHelper migrationEntityHelper, PreferencesManager preferencesManager)72 public DataMigrationManager( 73 Context userContext, 74 TransactionManager transactionManager, 75 HealthConnectPermissionHelper permissionHelper, 76 FirstGrantTimeManager firstGrantTimeManager, 77 DeviceInfoHelper deviceInfoHelper, 78 AppInfoHelper appInfoHelper, 79 HealthDataCategoryPriorityHelper healthDataCategoryPriorityHelper, 80 PriorityMigrationHelper priorityMigrationHelper, 81 MigrationEntityHelper migrationEntityHelper, 82 PreferencesManager preferencesManager) { 83 mUserContext = userContext; 84 mTransactionManager = transactionManager; 85 mPermissionHelper = permissionHelper; 86 mFirstGrantTimeManager = firstGrantTimeManager; 87 mDeviceInfoHelper = deviceInfoHelper; 88 mAppInfoHelper = appInfoHelper; 89 mHealthDataCategoryPriorityHelper = healthDataCategoryPriorityHelper; 90 mPriorityMigrationHelper = priorityMigrationHelper; 91 mMigrationEntityHelper = migrationEntityHelper; 92 mPreferencesManager = preferencesManager; 93 } 94 95 /** 96 * Parses and applies the provided migration entities. 97 * 98 * @param entities a collection of {@link MigrationEntity} to be applied. 99 */ apply(Collection<MigrationEntity> entities)100 public void apply(Collection<MigrationEntity> entities) throws EntityWriteException { 101 synchronized (sLock) { 102 mTransactionManager.runAsTransaction( 103 db -> { 104 // Grab the lock again to make sure error-prone is happy, and so that tests 105 // break if the following code is run asynchronously 106 synchronized (sLock) { 107 for (MigrationEntity entity : entities) { 108 migrateEntity(db, entity); 109 } 110 } 111 }); 112 } 113 } 114 115 /** Migrates the provided {@link MigrationEntity}. Must be called inside a DB transaction. */ 116 @GuardedBy("sLock") migrateEntity(SQLiteDatabase db, MigrationEntity entity)117 private void migrateEntity(SQLiteDatabase db, MigrationEntity entity) 118 throws EntityWriteException { 119 try { 120 if (checkEntityForDuplicates(db, entity)) { 121 return; 122 } 123 124 final MigrationPayload payload = entity.getPayload(); 125 if (payload instanceof RecordMigrationPayload) { 126 migrateRecord(db, (RecordMigrationPayload) payload); 127 } else if (payload instanceof PermissionMigrationPayload) { 128 migratePermissions((PermissionMigrationPayload) payload); 129 } else if (payload instanceof AppInfoMigrationPayload) { 130 migrateAppInfo((AppInfoMigrationPayload) payload); 131 } else if (payload instanceof PriorityMigrationPayload) { 132 migratePriority((PriorityMigrationPayload) payload); 133 } else if (payload instanceof MetadataMigrationPayload) { 134 migrateMetadata((MetadataMigrationPayload) payload); 135 } else { 136 throw new IllegalArgumentException("Unsupported payload type: " + payload); 137 } 138 } catch (RuntimeException e) { 139 throw new EntityWriteException(entity.getEntityId(), e); 140 } 141 } 142 143 @GuardedBy("sLock") migrateRecord(SQLiteDatabase db, RecordMigrationPayload payload)144 private void migrateRecord(SQLiteDatabase db, RecordMigrationPayload payload) { 145 long recordRowId = mTransactionManager.insertOrIgnoreOnConflict(db, parseRecord(payload)); 146 if (recordRowId != -1) { 147 mTransactionManager.insertOrIgnoreOnConflict( 148 db, ActivityDateHelper.getUpsertTableRequest(payload.getRecordInternal())); 149 } 150 } 151 parseRecord(RecordMigrationPayload payload)152 private UpsertTableRequest parseRecord(RecordMigrationPayload payload) { 153 final RecordInternal<?> record = payload.getRecordInternal(); 154 mAppInfoHelper.populateAppInfoId(record, /* requireAllFields */ false); 155 mDeviceInfoHelper.populateDeviceInfoId(record); 156 157 if (record.getUuid() == null) { 158 StorageUtils.addNameBasedUUIDTo(record); 159 } 160 161 return InternalHealthConnectMappings.getInstance() 162 .getRecordHelper(record.getRecordType()) 163 .getUpsertTableRequest(record); 164 } 165 166 @GuardedBy("sLock") migratePermissions(PermissionMigrationPayload payload)167 private void migratePermissions(PermissionMigrationPayload payload) { 168 final String packageName = payload.getHoldingPackageName(); 169 final List<String> permissions = payload.getPermissions(); 170 final UserHandle userHandle = mUserContext.getUser(); 171 172 if (permissions.isEmpty() 173 || mPermissionHelper.hasGrantedHealthPermissions(packageName, userHandle)) { 174 return; 175 } 176 177 final List<Exception> errors = new ArrayList<>(); 178 179 for (String permissionName : permissions) { 180 try { 181 mPermissionHelper.grantHealthPermission(packageName, permissionName, userHandle); 182 } catch (Exception e) { 183 errors.add(e); 184 } 185 } 186 187 // Throw if no permissions were migrated 188 if (errors.size() == permissions.size()) { 189 final RuntimeException error = 190 new RuntimeException( 191 "Error migrating permissions for " 192 + packageName 193 + ": " 194 + String.join(", ", payload.getPermissions())); 195 for (Exception e : errors) { 196 error.addSuppressed(e); 197 } 198 throw error; 199 } 200 201 mFirstGrantTimeManager.setFirstGrantTime( 202 packageName, payload.getFirstGrantTime(), userHandle); 203 } 204 205 @GuardedBy("sLock") migrateAppInfo(AppInfoMigrationPayload payload)206 private void migrateAppInfo(AppInfoMigrationPayload payload) { 207 mAppInfoHelper.updateAppInfoIfNotInstalled( 208 payload.getPackageName(), payload.getAppName(), payload.getAppIcon()); 209 } 210 211 /** 212 * Checks the provided entity for duplicates by {@code entityId}. Modifies {@link 213 * MigrationEntityHelper} table as a side effect. 214 * 215 * <p>Entities with the following payload types are exempt from deduplication checks (the result 216 * is always {@code false}): {@link RecordMigrationPayload}. 217 * 218 * @return {@code true} if the entity is duplicated and thus should be ignored, {@code false} 219 * otherwise. 220 */ 221 @GuardedBy("sLock") checkEntityForDuplicates(SQLiteDatabase db, MigrationEntity entity)222 private boolean checkEntityForDuplicates(SQLiteDatabase db, MigrationEntity entity) { 223 final MigrationPayload payload = entity.getPayload(); 224 225 if (payload instanceof RecordMigrationPayload) { 226 return false; // Do not deduplicate records by entityId 227 } 228 229 return !insertEntityIdIfNotPresent(db, entity.getEntityId()); 230 } 231 232 /** 233 * Inserts the provided {@code entity} into the database if it doesn't exist yet. Used for data 234 * deduplication. 235 * 236 * @return {@code true} if inserted successfully, {@code false} otherwise. 237 */ 238 @GuardedBy("sLock") insertEntityIdIfNotPresent(SQLiteDatabase db, String entityId)239 private boolean insertEntityIdIfNotPresent(SQLiteDatabase db, String entityId) { 240 final UpsertTableRequest request = mMigrationEntityHelper.getInsertRequest(entityId); 241 return mTransactionManager.insertOrIgnoreOnConflict(db, request) != -1; 242 } 243 244 /** Indicates an error during entity migration. */ 245 public static final class EntityWriteException extends Exception { 246 private final String mEntityId; 247 EntityWriteException(String entityId, @Nullable Throwable cause)248 private EntityWriteException(String entityId, @Nullable Throwable cause) { 249 super("Error writing entity: " + entityId, cause); 250 251 mEntityId = entityId; 252 } 253 254 /** 255 * Returns an identifier of the failed entity, as specified in {@link 256 * MigrationEntity#getEntityId()}. 257 */ getEntityId()258 public String getEntityId() { 259 return mEntityId; 260 } 261 } 262 263 /** 264 * Internal method to migrate priority list of packages for data category 265 * 266 * @param priorityMigrationPayload contains data category and priority list 267 */ migratePriority(PriorityMigrationPayload priorityMigrationPayload)268 private void migratePriority(PriorityMigrationPayload priorityMigrationPayload) { 269 if (priorityMigrationPayload.getDataOrigins().isEmpty()) { 270 return; 271 } 272 273 List<String> priorityToMigrate = 274 priorityMigrationPayload.getDataOrigins().stream() 275 .map(dataOrigin -> dataOrigin.getPackageName()) 276 .toList(); 277 278 List<String> preMigrationPriority = 279 mAppInfoHelper.getPackageNames( 280 mPriorityMigrationHelper.getPreMigrationPriority( 281 priorityMigrationPayload.getDataCategory())); 282 283 /* 284 The combined priority would contain priority order from module appended by additional 285 packages from apk priority order. 286 */ 287 List<String> combinedPriorityOrder = 288 Stream.concat(preMigrationPriority.stream(), priorityToMigrate.stream()) 289 .distinct() 290 .collect(Collectors.toList()); 291 292 /* 293 * setPriorityOrder removes any additional packages that were not present already in 294 * priority, and it adds any package in priority that was present earlier but missing in 295 * updated priority. This means it will remove any package that don't have required 296 * permission for category as well as it will remove any package that is uninstalled. 297 */ 298 mHealthDataCategoryPriorityHelper.setPriorityOrder( 299 priorityMigrationPayload.getDataCategory(), combinedPriorityOrder); 300 } 301 302 /** 303 * Migrates Metadata like recordRetentionPeriod 304 * 305 * @param payload of type MetadataMigrationPayload having retention period. 306 */ migrateMetadata(MetadataMigrationPayload payload)307 private void migrateMetadata(MetadataMigrationPayload payload) { 308 mPreferencesManager.setRecordRetentionPeriodInDays(payload.getRecordRetentionPeriodDays()); 309 } 310 } 311