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