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