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.fitness; 18 19 import static android.health.connect.accesslog.AccessLog.OperationType.OPERATION_TYPE_DELETE; 20 import static android.health.connect.accesslog.AccessLog.OperationType.OPERATION_TYPE_UPSERT; 21 22 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.APP_INFO_ID_COLUMN_NAME; 23 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.UUID_COLUMN_NAME; 24 25 import static java.util.Collections.singletonList; 26 import static java.util.Objects.requireNonNull; 27 28 import android.annotation.Nullable; 29 import android.database.Cursor; 30 import android.health.connect.RecordIdFilter; 31 import android.health.connect.aidl.DeleteUsingFiltersRequestParcel; 32 import android.health.connect.internal.datatypes.utils.HealthConnectMappings; 33 import android.util.ArrayMap; 34 import android.util.ArraySet; 35 36 import com.android.healthfitness.flags.Flags; 37 import com.android.server.healthconnect.storage.TransactionManager; 38 import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper; 39 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper; 40 import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsHelper; 41 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper; 42 import com.android.server.healthconnect.storage.request.DeleteTableRequest; 43 import com.android.server.healthconnect.storage.request.ReadTableRequest; 44 import com.android.server.healthconnect.storage.request.UpsertTableRequest; 45 import com.android.server.healthconnect.storage.utils.InternalHealthConnectMappings; 46 import com.android.server.healthconnect.storage.utils.StorageUtils; 47 48 import java.time.Instant; 49 import java.util.ArrayList; 50 import java.util.HashSet; 51 import java.util.List; 52 import java.util.Map; 53 import java.util.Objects; 54 import java.util.Set; 55 import java.util.UUID; 56 57 /** 58 * Delete FitnessRecords from the database based on the given request, using the TransactionManager. 59 * 60 * <p>FitnessRecord refers to any of the record types defined in {@link 61 * android.health.connect.datatypes.RecordTypeIdentifier}; 62 * 63 * @hide 64 */ 65 public final class FitnessRecordDeleteHelper { 66 private static final String TAG = "HealthConnectFitnessDelete"; 67 68 private final TransactionManager mTransactionManager; 69 private final AppInfoHelper mAppInfoHelper; 70 private final AccessLogsHelper mAccessLogsHelper; 71 private final HealthConnectMappings mHealthConnectMappings; 72 private final InternalHealthConnectMappings mInternalHealthConnectMappings; 73 FitnessRecordDeleteHelper( TransactionManager transactionManager, AppInfoHelper appInfoHelper, AccessLogsHelper accessLogsHelper, InternalHealthConnectMappings internalHealthConnectMappings)74 public FitnessRecordDeleteHelper( 75 TransactionManager transactionManager, 76 AppInfoHelper appInfoHelper, 77 AccessLogsHelper accessLogsHelper, 78 InternalHealthConnectMappings internalHealthConnectMappings) { 79 mTransactionManager = transactionManager; 80 mAppInfoHelper = appInfoHelper; 81 mAccessLogsHelper = accessLogsHelper; 82 mHealthConnectMappings = internalHealthConnectMappings.getExternalMappings(); 83 mInternalHealthConnectMappings = internalHealthConnectMappings; 84 } 85 86 /** 87 * Delete records specified by the given request. 88 * 89 * @param callingPackageName The package name trying to delete the records. 90 * @param request The request that specifies what to delete. 91 * @param enforceSelfDelete Whether the caller should only be able to delete their own data. 92 * @param shouldRecordAccessLog Whether access logs should be recorded for this call 93 * @return number of records deleted. 94 */ deleteRecords( String callingPackageName, DeleteUsingFiltersRequestParcel request, boolean enforceSelfDelete, boolean shouldRecordAccessLog)95 public int deleteRecords( 96 String callingPackageName, 97 DeleteUsingFiltersRequestParcel request, 98 boolean enforceSelfDelete, 99 boolean shouldRecordAccessLog) { 100 if (request.usesIdFilters() && request.usesNonIdFilters()) { 101 throw new IllegalArgumentException( 102 "Requests with both id and non-id filters are not" + " supported"); 103 } 104 105 if (enforceSelfDelete) { 106 request.setPackageNameFilters(singletonList(callingPackageName)); 107 } 108 109 if (request.usesIdFilters()) { 110 return deleteByIdFilter( 111 callingPackageName, request, enforceSelfDelete, shouldRecordAccessLog); 112 } else { 113 return deleteByNonIdFilter(callingPackageName, request, shouldRecordAccessLog); 114 } 115 } 116 deleteByIdFilter( String callingPackageName, DeleteUsingFiltersRequestParcel request, boolean enforceSelfDelete, boolean shouldRecordAccessLog)117 private int deleteByIdFilter( 118 String callingPackageName, 119 DeleteUsingFiltersRequestParcel request, 120 boolean enforceSelfDelete, 121 boolean shouldRecordAccessLog) { 122 List<DeleteTableRequest> deleteTableRequests = 123 new ArrayList<>(request.getRecordTypeFilters().size()); 124 Set<Integer> recordTypeIds = new HashSet<>(); 125 126 List<RecordIdFilter> recordIds = request.getRecordIdFiltersParcel().getRecordIdFilters(); 127 Set<UUID> uuidSet = new ArraySet<>(); 128 Map<RecordHelper<?>, List<UUID>> recordTypeToUuids = new ArrayMap<>(); 129 for (RecordIdFilter recordId : recordIds) { 130 RecordHelper<?> recordHelper = 131 mInternalHealthConnectMappings.getRecordHelper( 132 mHealthConnectMappings.getRecordType(recordId.getRecordType())); 133 UUID uuid = StorageUtils.getUUIDFor(recordId, callingPackageName); 134 if (uuidSet.contains(uuid)) { 135 // id has been already been processed; 136 continue; 137 } 138 recordTypeToUuids.putIfAbsent(recordHelper, new ArrayList<>()); 139 Objects.requireNonNull(recordTypeToUuids.get(recordHelper)).add(uuid); 140 uuidSet.add(uuid); 141 } 142 143 recordTypeToUuids.forEach( 144 (recordHelper, uuids) -> { 145 deleteTableRequests.add(recordHelper.getDeleteTableRequest(uuids)); 146 recordTypeIds.add(recordHelper.getRecordIdentifier()); 147 }); 148 149 return delete( 150 callingPackageName, 151 deleteTableRequests, 152 recordTypeIds, 153 shouldRecordAccessLog, 154 enforceSelfDelete); 155 } 156 deleteByNonIdFilter( String callingPackageName, DeleteUsingFiltersRequestParcel request, boolean shouldRecordAccessLog)157 private int deleteByNonIdFilter( 158 String callingPackageName, 159 DeleteUsingFiltersRequestParcel request, 160 boolean shouldRecordAccessLog) { 161 List<DeleteTableRequest> deleteTableRequests = 162 new ArrayList<>(request.getRecordTypeFilters().size()); 163 Set<Integer> recordTypeIds = new HashSet<>(); 164 165 List<Integer> recordTypeFilters = request.getRecordTypeFilters(); 166 if (recordTypeFilters == null || recordTypeFilters.isEmpty()) { 167 recordTypeFilters = 168 new ArrayList<>( 169 HealthConnectMappings.getInstance() 170 .getRecordIdToExternalRecordClassMap() 171 .keySet()); 172 } 173 174 recordTypeFilters.forEach( 175 (recordType) -> { 176 RecordHelper<?> recordHelper = 177 mInternalHealthConnectMappings.getRecordHelper(recordType); 178 179 deleteTableRequests.add( 180 recordHelper.getDeleteTableRequest( 181 request.getPackageNameFilters(), 182 request.getStartTime(), 183 request.getEndTime(), 184 request.isLocalTimeFilter(), 185 mAppInfoHelper)); 186 recordTypeIds.add(recordHelper.getRecordIdentifier()); 187 }); 188 189 return delete( 190 callingPackageName, 191 deleteTableRequests, 192 recordTypeIds, 193 shouldRecordAccessLog, 194 // Always send false here, since we set the package filters in the request itself. 195 /* enforceSelfDelete= */ false); 196 } 197 delete( @ullable String callingPackageName, List<DeleteTableRequest> deleteTableRequests, @Nullable Set<Integer> recordTypeIds, boolean shouldRecordAccessLog, boolean enforceSelfDelete)198 private int delete( 199 @Nullable String callingPackageName, 200 List<DeleteTableRequest> deleteTableRequests, 201 @Nullable Set<Integer> recordTypeIds, 202 boolean shouldRecordAccessLog, 203 boolean enforceSelfDelete) { 204 if (shouldRecordAccessLog) { 205 Objects.requireNonNull(recordTypeIds); 206 } 207 if (shouldRecordAccessLog || enforceSelfDelete) { 208 Objects.requireNonNull(callingPackageName); 209 } 210 211 long currentTime = Instant.now().toEpochMilli(); 212 ChangeLogsHelper.ChangeLogs deletionChangelogs = 213 new ChangeLogsHelper.ChangeLogs(OPERATION_TYPE_DELETE, currentTime); 214 ChangeLogsHelper.ChangeLogs modificationChangelogs = 215 new ChangeLogsHelper.ChangeLogs(OPERATION_TYPE_UPSERT, currentTime); 216 217 return mTransactionManager.runAsTransaction( 218 db -> { 219 int numberOfRecordsDeleted = 0; 220 for (DeleteTableRequest deleteTableRequest : deleteTableRequests) { 221 final RecordHelper<?> recordHelper = 222 mInternalHealthConnectMappings.getRecordHelper( 223 deleteTableRequest.getRecordType()); 224 225 // We first always read the records for: 226 // (1) generating changelogs 227 // (2) logging number of records deleted 228 try (Cursor cursor = 229 db.rawQuery(deleteTableRequest.getReadCommand(), null)) { 230 while (cursor.moveToNext()) { 231 String packageColumnName = 232 requireNonNull(deleteTableRequest.getPackageColumnName()); 233 String idColumnName = 234 requireNonNull(deleteTableRequest.getIdColumnName()); 235 numberOfRecordsDeleted++; 236 long readDataAppInfoId = 237 StorageUtils.getCursorLong(cursor, packageColumnName); 238 if (enforceSelfDelete) { 239 enforcePackageCheck( 240 StorageUtils.getCursorUUID(cursor, idColumnName), 241 readDataAppInfoId, 242 Objects.requireNonNull(callingPackageName)); 243 } 244 UUID deletedRecordUuid = 245 StorageUtils.getCursorUUID(cursor, idColumnName); 246 deletionChangelogs.addUUID( 247 deleteTableRequest.getRecordType(), 248 readDataAppInfoId, 249 deletedRecordUuid); 250 251 // Add changelogs for affected records, e.g. a training plan 252 // being deleted will create changelogs for affected exercise 253 // sessions. 254 for (ReadTableRequest additionalChangelogUuidRequest : 255 recordHelper.getReadRequestsForRecordsModifiedByDeletion( 256 deletedRecordUuid)) { 257 Cursor cursorAdditionalUuids = 258 mTransactionManager.read( 259 additionalChangelogUuidRequest); 260 while (cursorAdditionalUuids.moveToNext()) { 261 modificationChangelogs.addUUID( 262 requireNonNull( 263 additionalChangelogUuidRequest 264 .getRecordHelper()) 265 .getRecordIdentifier(), 266 StorageUtils.getCursorLong( 267 cursorAdditionalUuids, 268 APP_INFO_ID_COLUMN_NAME), 269 StorageUtils.getCursorUUID( 270 cursorAdditionalUuids, UUID_COLUMN_NAME)); 271 } 272 cursorAdditionalUuids.close(); 273 } 274 } 275 } 276 db.execSQL(deleteTableRequest.getDeleteCommand()); 277 } 278 279 for (UpsertTableRequest insertRequestsForChangeLog : 280 deletionChangelogs.getUpsertTableRequests()) { 281 mTransactionManager.insert(db, insertRequestsForChangeLog); 282 } 283 for (UpsertTableRequest modificationChangelog : 284 modificationChangelogs.getUpsertTableRequests()) { 285 mTransactionManager.insert(db, modificationChangelog); 286 } 287 if (Flags.addMissingAccessLogs() && shouldRecordAccessLog) { 288 mAccessLogsHelper.recordDeleteAccessLog( 289 db, 290 Objects.requireNonNull(callingPackageName), 291 Objects.requireNonNull(recordTypeIds)); 292 } 293 return numberOfRecordsDeleted; 294 }); 295 } 296 297 /** 298 * Delete records for the given deleteTableRequests. 299 * 300 * @param deleteTableRequests list of delete requests for a record table. 301 */ deleteRecordsUnrestricted(List<DeleteTableRequest> deleteTableRequests)302 public void deleteRecordsUnrestricted(List<DeleteTableRequest> deleteTableRequests) { 303 delete( 304 /* callingPackageName= */ null, 305 deleteTableRequests, 306 /* recordTypeIds= */ null, 307 /* shouldRecordAccessLog= */ false, 308 /* enforceSelfDelete= */ false); 309 } 310 enforcePackageCheck(UUID uuid, long readDataAppInfoId, String callingPackageName)311 private void enforcePackageCheck(UUID uuid, long readDataAppInfoId, String callingPackageName) { 312 long callingAppInfoId = mAppInfoHelper.getAppInfoId(callingPackageName); 313 if (callingAppInfoId != readDataAppInfoId) { 314 throw new IllegalArgumentException(callingAppInfoId + " is not the owner for " + uuid); 315 } 316 } 317 } 318