• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;
18 
19 import static android.Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA;
20 import static android.content.pm.PackageManager.PERMISSION_GRANTED;
21 import static android.health.connect.Constants.DEFAULT_LONG;
22 import static android.health.connect.Constants.READ;
23 import static android.health.connect.HealthConnectException.ERROR_INTERNAL;
24 import static android.health.connect.HealthConnectException.ERROR_SECURITY;
25 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
26 
27 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.DELETE_DATA;
28 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.GET_CHANGES;
29 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.GET_CHANGES_TOKEN;
30 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.INSERT_DATA;
31 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.READ_AGGREGATED_DATA;
32 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.READ_DATA;
33 import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.UPDATE_DATA;
34 
35 import android.Manifest;
36 import android.annotation.NonNull;
37 import android.annotation.Nullable;
38 import android.content.AttributionSource;
39 import android.content.Context;
40 import android.content.Intent;
41 import android.content.pm.PackageManager;
42 import android.content.pm.ResolveInfo;
43 import android.database.sqlite.SQLiteException;
44 import android.health.connect.Constants;
45 import android.health.connect.FetchDataOriginsPriorityOrderResponse;
46 import android.health.connect.HealthConnectDataState;
47 import android.health.connect.HealthConnectException;
48 import android.health.connect.HealthConnectManager;
49 import android.health.connect.HealthConnectManager.DataDownloadState;
50 import android.health.connect.HealthDataCategory;
51 import android.health.connect.HealthPermissions;
52 import android.health.connect.RecordTypeInfoResponse;
53 import android.health.connect.accesslog.AccessLog;
54 import android.health.connect.accesslog.AccessLogsResponseParcel;
55 import android.health.connect.aidl.ActivityDatesRequestParcel;
56 import android.health.connect.aidl.ActivityDatesResponseParcel;
57 import android.health.connect.aidl.AggregateDataRequestParcel;
58 import android.health.connect.aidl.ApplicationInfoResponseParcel;
59 import android.health.connect.aidl.DeleteUsingFiltersRequestParcel;
60 import android.health.connect.aidl.GetPriorityResponseParcel;
61 import android.health.connect.aidl.HealthConnectExceptionParcel;
62 import android.health.connect.aidl.IAccessLogsResponseCallback;
63 import android.health.connect.aidl.IActivityDatesResponseCallback;
64 import android.health.connect.aidl.IAggregateRecordsResponseCallback;
65 import android.health.connect.aidl.IApplicationInfoResponseCallback;
66 import android.health.connect.aidl.IChangeLogsResponseCallback;
67 import android.health.connect.aidl.IDataStagingFinishedCallback;
68 import android.health.connect.aidl.IEmptyResponseCallback;
69 import android.health.connect.aidl.IGetChangeLogTokenCallback;
70 import android.health.connect.aidl.IGetHealthConnectDataStateCallback;
71 import android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback;
72 import android.health.connect.aidl.IGetPriorityResponseCallback;
73 import android.health.connect.aidl.IHealthConnectService;
74 import android.health.connect.aidl.IInsertRecordsResponseCallback;
75 import android.health.connect.aidl.IMigrationCallback;
76 import android.health.connect.aidl.IReadRecordsResponseCallback;
77 import android.health.connect.aidl.IRecordTypeInfoResponseCallback;
78 import android.health.connect.aidl.InsertRecordsResponseParcel;
79 import android.health.connect.aidl.ReadRecordsRequestParcel;
80 import android.health.connect.aidl.ReadRecordsResponseParcel;
81 import android.health.connect.aidl.RecordIdFiltersParcel;
82 import android.health.connect.aidl.RecordTypeInfoResponseParcel;
83 import android.health.connect.aidl.RecordsParcel;
84 import android.health.connect.aidl.UpdatePriorityRequestParcel;
85 import android.health.connect.changelog.ChangeLogTokenRequest;
86 import android.health.connect.changelog.ChangeLogTokenResponse;
87 import android.health.connect.changelog.ChangeLogsRequest;
88 import android.health.connect.changelog.ChangeLogsResponse;
89 import android.health.connect.changelog.ChangeLogsResponse.DeletedLog;
90 import android.health.connect.datatypes.AppInfo;
91 import android.health.connect.datatypes.DataOrigin;
92 import android.health.connect.datatypes.Record;
93 import android.health.connect.internal.datatypes.RecordInternal;
94 import android.health.connect.internal.datatypes.utils.AggregationTypeIdMapper;
95 import android.health.connect.internal.datatypes.utils.RecordMapper;
96 import android.health.connect.internal.datatypes.utils.RecordTypePermissionCategoryMapper;
97 import android.health.connect.migration.HealthConnectMigrationUiState;
98 import android.health.connect.migration.MigrationEntityParcel;
99 import android.health.connect.migration.MigrationException;
100 import android.health.connect.ratelimiter.RateLimiter;
101 import android.health.connect.ratelimiter.RateLimiter.QuotaCategory;
102 import android.health.connect.ratelimiter.RateLimiterException;
103 import android.health.connect.restore.BackupFileNamesSet;
104 import android.health.connect.restore.StageRemoteDataException;
105 import android.health.connect.restore.StageRemoteDataRequest;
106 import android.os.Binder;
107 import android.os.ParcelFileDescriptor;
108 import android.os.Process;
109 import android.os.RemoteException;
110 import android.os.Trace;
111 import android.os.UserHandle;
112 import android.permission.PermissionManager;
113 import android.util.ArrayMap;
114 import android.util.ArraySet;
115 import android.util.Log;
116 import android.util.Pair;
117 import android.util.Slog;
118 
119 import com.android.internal.annotations.VisibleForTesting;
120 import com.android.server.LocalManagerRegistry;
121 import com.android.server.appop.AppOpsManagerLocal;
122 import com.android.server.healthconnect.backuprestore.BackupRestore;
123 import com.android.server.healthconnect.logging.HealthConnectServiceLogger;
124 import com.android.server.healthconnect.migration.DataMigrationManager;
125 import com.android.server.healthconnect.migration.MigrationCleaner;
126 import com.android.server.healthconnect.migration.MigrationStateManager;
127 import com.android.server.healthconnect.migration.MigrationUiStateManager;
128 import com.android.server.healthconnect.migration.PriorityMigrationHelper;
129 import com.android.server.healthconnect.permission.DataPermissionEnforcer;
130 import com.android.server.healthconnect.permission.FirstGrantTimeManager;
131 import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
132 import com.android.server.healthconnect.storage.AutoDeleteService;
133 import com.android.server.healthconnect.storage.TransactionManager;
134 import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper;
135 import com.android.server.healthconnect.storage.datatypehelpers.ActivityDateHelper;
136 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
137 import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsHelper;
138 import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsRequestHelper;
139 import com.android.server.healthconnect.storage.datatypehelpers.DeviceInfoHelper;
140 import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
141 import com.android.server.healthconnect.storage.datatypehelpers.MigrationEntityHelper;
142 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
143 import com.android.server.healthconnect.storage.request.AggregateTransactionRequest;
144 import com.android.server.healthconnect.storage.request.DeleteTransactionRequest;
145 import com.android.server.healthconnect.storage.request.ReadTransactionRequest;
146 import com.android.server.healthconnect.storage.request.UpsertTransactionRequest;
147 import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
148 
149 import java.io.IOException;
150 import java.time.Instant;
151 import java.time.LocalDate;
152 import java.util.ArrayList;
153 import java.util.Collections;
154 import java.util.HashSet;
155 import java.util.List;
156 import java.util.Map;
157 import java.util.Map.Entry;
158 import java.util.Objects;
159 import java.util.Set;
160 import java.util.concurrent.atomic.AtomicBoolean;
161 import java.util.stream.Collectors;
162 
163 /**
164  * IHealthConnectService's implementation
165  *
166  * @hide
167  */
168 final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
169     private static final String TAG = "HealthConnectService";
170     // Permission for test api for deleting staged data
171     private static final String DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA_PERMISSION =
172             "android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA";
173     // Allows an application to act as a backup inter-agent to send and receive HealthConnect data
174     private static final String HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION =
175             "android.permission.HEALTH_CONNECT_BACKUP_INTER_AGENT";
176 
177     private static final String TAG_INSERT = "HealthConnectInsert";
178     private static final String TAG_READ = "HealthConnectRead";
179     private static final String TAG_GRANT_PERMISSION = "HealthConnectGrantReadPermissions";
180     private static final String TAG_READ_PERMISSION = "HealthConnectReadPermission";
181     private static final String TAG_INSERT_SUBTASKS = "HealthConnectInsertSubtasks";
182 
183     private static final String TAG_DELETE_SUBTASKS = "HealthConnectDeleteSubtasks";
184     private static final String TAG_READ_SUBTASKS = "HealthConnectReadSubtasks";
185     private static final int TRACE_TAG_INSERT = TAG_INSERT.hashCode();
186     private static final int TRACE_TAG_READ = TAG_READ.hashCode();
187     private static final int TRACE_TAG_GRANT_PERMISSION = TAG_GRANT_PERMISSION.hashCode();
188     private static final int TRACE_TAG_READ_PERMISSION = TAG_READ_PERMISSION.hashCode();
189     private static final int TRACE_TAG_INSERT_SUBTASKS = TAG_INSERT_SUBTASKS.hashCode();
190     private static final int TRACE_TAG_DELETE_SUBTASKS = TAG_DELETE_SUBTASKS.hashCode();
191     private static final int TRACE_TAG_READ_SUBTASKS = TAG_READ_SUBTASKS.hashCode();
192 
193     private final TransactionManager mTransactionManager;
194     private final HealthConnectPermissionHelper mPermissionHelper;
195     private final FirstGrantTimeManager mFirstGrantTimeManager;
196     private final Context mContext;
197     private final PermissionManager mPermissionManager;
198 
199     private final BackupRestore mBackupRestore;
200     private final MigrationStateManager mMigrationStateManager;
201 
202     private final DataPermissionEnforcer mDataPermissionEnforcer;
203 
204     private final AppOpsManagerLocal mAppOpsManagerLocal;
205     private final MigrationUiStateManager mMigrationUiStateManager;
206 
207     private volatile UserHandle mCurrentForegroundUser;
208 
HealthConnectServiceImpl( TransactionManager transactionManager, HealthConnectPermissionHelper permissionHelper, MigrationCleaner migrationCleaner, FirstGrantTimeManager firstGrantTimeManager, MigrationStateManager migrationStateManager, MigrationUiStateManager migrationUiStateManager, Context context)209     HealthConnectServiceImpl(
210             TransactionManager transactionManager,
211             HealthConnectPermissionHelper permissionHelper,
212             MigrationCleaner migrationCleaner,
213             FirstGrantTimeManager firstGrantTimeManager,
214             MigrationStateManager migrationStateManager,
215             MigrationUiStateManager migrationUiStateManager,
216             Context context) {
217         mTransactionManager = transactionManager;
218         mPermissionHelper = permissionHelper;
219         mFirstGrantTimeManager = firstGrantTimeManager;
220         mContext = context;
221         mCurrentForegroundUser = context.getUser();
222         mPermissionManager = mContext.getSystemService(PermissionManager.class);
223         mMigrationStateManager = migrationStateManager;
224         mDataPermissionEnforcer = new DataPermissionEnforcer(mPermissionManager, mContext);
225         mAppOpsManagerLocal = LocalManagerRegistry.getManager(AppOpsManagerLocal.class);
226         mBackupRestore =
227                 new BackupRestore(mFirstGrantTimeManager, mMigrationStateManager, mContext);
228         mMigrationUiStateManager = migrationUiStateManager;
229         migrationCleaner.attachTo(migrationStateManager);
230         mMigrationUiStateManager.attachTo(migrationStateManager);
231     }
232 
onUserSwitching(UserHandle currentForegroundUser)233     public void onUserSwitching(UserHandle currentForegroundUser) {
234         mCurrentForegroundUser = currentForegroundUser;
235         mBackupRestore.setupForUser(currentForegroundUser);
236     }
237 
238     @Override
grantHealthPermission( @onNull String packageName, @NonNull String permissionName, @NonNull UserHandle user)239     public void grantHealthPermission(
240             @NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) {
241         throwIllegalStateExceptionIfDataSyncInProgress();
242         Trace.traceBegin(TRACE_TAG_GRANT_PERMISSION, TAG_GRANT_PERMISSION);
243         mPermissionHelper.grantHealthPermission(packageName, permissionName, user);
244         Trace.traceEnd(TRACE_TAG_GRANT_PERMISSION);
245     }
246 
247     @Override
revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason, @NonNull UserHandle user)248     public void revokeHealthPermission(
249             @NonNull String packageName,
250             @NonNull String permissionName,
251             @Nullable String reason,
252             @NonNull UserHandle user) {
253         throwIllegalStateExceptionIfDataSyncInProgress();
254         mPermissionHelper.revokeHealthPermission(packageName, permissionName, reason, user);
255     }
256 
257     @Override
revokeAllHealthPermissions( @onNull String packageName, @Nullable String reason, @NonNull UserHandle user)258     public void revokeAllHealthPermissions(
259             @NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) {
260         throwIllegalStateExceptionIfDataSyncInProgress();
261         mPermissionHelper.revokeAllHealthPermissions(packageName, reason, user);
262     }
263 
264     @Override
getGrantedHealthPermissions( @onNull String packageName, @NonNull UserHandle user)265     public List<String> getGrantedHealthPermissions(
266             @NonNull String packageName, @NonNull UserHandle user) {
267         throwIllegalStateExceptionIfDataSyncInProgress();
268         Trace.traceBegin(TRACE_TAG_READ_PERMISSION, TAG_READ_PERMISSION);
269         List<String> grantedPermissions =
270                 mPermissionHelper.getGrantedHealthPermissions(packageName, user);
271         Trace.traceEnd(TRACE_TAG_READ_PERMISSION);
272         return grantedPermissions;
273     }
274 
275     @Override
getHistoricalAccessStartDateInMilliseconds( @onNull String packageName, @NonNull UserHandle userHandle)276     public long getHistoricalAccessStartDateInMilliseconds(
277             @NonNull String packageName, @NonNull UserHandle userHandle) {
278         throwIllegalStateExceptionIfDataSyncInProgress();
279         Instant date = mPermissionHelper.getHealthDataStartDateAccess(packageName, userHandle);
280         if (date == null) {
281             return Constants.DEFAULT_LONG;
282         } else {
283             return date.toEpochMilli();
284         }
285     }
286 
287     /**
288      * Inserts {@code recordsParcel} into the HealthConnect database.
289      *
290      * @param recordsParcel parcel for list of records to be inserted.
291      * @param callback Callback to receive result of performing this operation. The keys returned in
292      *     {@link InsertRecordsResponseParcel} are the unique IDs of the input records. The values
293      *     are in same order as {@code record}. In case of an error or a permission failure the
294      *     HealthConnect service, {@link IInsertRecordsResponseCallback#onError} will be invoked
295      *     with a {@link HealthConnectExceptionParcel}.
296      */
297     @Override
insertRecords( @onNull AttributionSource attributionSource, @NonNull RecordsParcel recordsParcel, @NonNull IInsertRecordsResponseCallback callback)298     public void insertRecords(
299             @NonNull AttributionSource attributionSource,
300             @NonNull RecordsParcel recordsParcel,
301             @NonNull IInsertRecordsResponseCallback callback) {
302         final int uid = Binder.getCallingUid();
303         final int pid = Binder.getCallingPid();
304         final UserHandle userHandle = Binder.getCallingUserHandle();
305         final HealthConnectServiceLogger.Builder builder =
306                 new HealthConnectServiceLogger.Builder(false, INSERT_DATA)
307                         .setPackageName(attributionSource.getPackageName());
308 
309         HealthConnectThreadScheduler.schedule(
310                 mContext,
311                 () -> {
312                     try {
313                         enforceIsForegroundUser(userHandle);
314                         verifyPackageNameFromUid(uid, attributionSource);
315                         if (hasDataManagementPermission(uid, pid)) {
316                             throw new SecurityException(
317                                     "Apps with android.permission.MANAGE_HEALTH_DATA permission are"
318                                             + " not allowed to insert records");
319                         }
320                         enforceMemoryRateLimit(
321                                 recordsParcel.getRecordsSize(),
322                                 recordsParcel.getRecordsChunkSize());
323                         final List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
324                         builder.setNumberOfRecords(recordInternals.size());
325                         throwExceptionIfDataSyncInProgress();
326                         mDataPermissionEnforcer.enforceRecordsWritePermissions(
327                                 recordInternals, attributionSource);
328                         boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
329                         tryAcquireApiCallQuota(
330                                 uid, QuotaCategory.QUOTA_CATEGORY_WRITE, isInForeground, builder);
331                         Trace.traceBegin(TRACE_TAG_INSERT, TAG_INSERT);
332                         UpsertTransactionRequest insertRequest =
333                                 new UpsertTransactionRequest(
334                                         attributionSource.getPackageName(),
335                                         recordInternals,
336                                         mContext,
337                                         /* isInsertRequest */ true,
338                                         mDataPermissionEnforcer
339                                                 .collectExtraWritePermissionStateMapping(
340                                                         recordInternals, attributionSource));
341                         List<String> uuids = mTransactionManager.insertAll(insertRequest);
342                         tryAndReturnResult(callback, uuids, builder);
343 
344                         HealthConnectThreadScheduler.scheduleInternalTask(
345                                 () -> postInsertTasks(attributionSource, recordsParcel));
346 
347                         finishDataDeliveryWriteRecords(recordInternals, attributionSource);
348                         logRecordTypeSpecificUpsertMetrics(
349                                 recordInternals, attributionSource.getPackageName());
350                         builder.setDataTypesFromRecordInternals(recordInternals);
351                     } catch (SQLiteException sqLiteException) {
352                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
353                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
354                         tryAndThrowException(
355                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
356                     } catch (SecurityException securityException) {
357                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
358                         Slog.e(TAG, "SecurityException: ", securityException);
359                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
360                     } catch (HealthConnectException healthConnectException) {
361                         builder.setHealthDataServiceApiStatusError(
362                                 healthConnectException.getErrorCode());
363                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
364                         tryAndThrowException(
365                                 callback,
366                                 healthConnectException,
367                                 healthConnectException.getErrorCode());
368                     } catch (Exception e) {
369                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
370                         Slog.e(TAG, "Exception: ", e);
371                         tryAndThrowException(callback, e, ERROR_INTERNAL);
372                     } finally {
373                         Trace.traceEnd(TRACE_TAG_INSERT);
374                         builder.build().log();
375                     }
376                 },
377                 uid,
378                 false);
379     }
380 
postInsertTasks( @onNull AttributionSource attributionSource, @NonNull RecordsParcel recordsParcel)381     private void postInsertTasks(
382             @NonNull AttributionSource attributionSource, @NonNull RecordsParcel recordsParcel) {
383         Trace.traceBegin(TRACE_TAG_INSERT_SUBTASKS, TAG_INSERT.concat("PostInsertTasks"));
384 
385         ActivityDateHelper.getInstance().insertRecordDate(recordsParcel.getRecords());
386         Set<Integer> recordsTypesInsertedSet =
387                 recordsParcel.getRecords().stream()
388                         .map(RecordInternal::getRecordType)
389                         .collect(Collectors.toSet());
390         // Update AppInfo table with the record types of records inserted in the request for the
391         // current package.
392         AppInfoHelper.getInstance()
393                 .updateAppInfoRecordTypesUsedOnInsert(
394                         recordsTypesInsertedSet, attributionSource.getPackageName());
395 
396         Trace.traceEnd(TRACE_TAG_INSERT_SUBTASKS);
397     }
398 
399     /**
400      * Returns aggregation results based on the {@code request} into the HealthConnect database.
401      *
402      * @param request represents the request using which the aggregation is to be performed.
403      * @param callback Callback to receive result of performing this operation.
404      */
aggregateRecords( @onNull AttributionSource attributionSource, AggregateDataRequestParcel request, IAggregateRecordsResponseCallback callback)405     public void aggregateRecords(
406             @NonNull AttributionSource attributionSource,
407             AggregateDataRequestParcel request,
408             IAggregateRecordsResponseCallback callback) {
409         final int uid = Binder.getCallingUid();
410         final int pid = Binder.getCallingPid();
411         final UserHandle userHandle = Binder.getCallingUserHandle();
412         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
413         final HealthConnectServiceLogger.Builder builder =
414                 new HealthConnectServiceLogger.Builder(
415                                 holdsDataManagementPermission, READ_AGGREGATED_DATA)
416                         .setPackageName(attributionSource.getPackageName());
417 
418         HealthConnectThreadScheduler.schedule(
419                 mContext,
420                 () -> {
421                     try {
422                         enforceIsForegroundUser(userHandle);
423                         verifyPackageNameFromUid(uid, attributionSource);
424                         builder.setNumberOfRecords(request.getAggregateIds().length);
425                         throwExceptionIfDataSyncInProgress();
426                         List<Integer> recordTypesToTest = new ArrayList<>();
427                         for (int aggregateId : request.getAggregateIds()) {
428                             recordTypesToTest.addAll(
429                                     AggregationTypeIdMapper.getInstance()
430                                             .getAggregationTypeFor(aggregateId)
431                                             .getApplicableRecordTypeIds());
432                         }
433 
434                         if (!holdsDataManagementPermission) {
435                             boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
436                             if (!isInForeground) {
437                                 throwSecurityException(
438                                         attributionSource.getPackageName()
439                                                 + "must be in foreground to call aggregate method");
440                             }
441                             mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
442                                     recordTypesToTest, attributionSource);
443                             tryAcquireApiCallQuota(
444                                     uid,
445                                     RateLimiter.QuotaCategory.QUOTA_CATEGORY_READ,
446                                     isInForeground,
447                                     builder);
448                         }
449                         callback.onResult(
450                                 new AggregateTransactionRequest(
451                                                 attributionSource.getPackageName(), request)
452                                         .getAggregateDataResponseParcel());
453                         finishDataDeliveryRead(recordTypesToTest, attributionSource);
454                         builder.setDataTypesFromRecordTypes(recordTypesToTest)
455                                 .setHealthDataServiceApiStatusSuccess();
456                     } catch (SQLiteException sqLiteException) {
457                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
458                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
459                         tryAndThrowException(
460                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
461                     } catch (SecurityException securityException) {
462                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
463                         Slog.e(TAG, "SecurityException: ", securityException);
464                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
465                     } catch (HealthConnectException healthConnectException) {
466                         builder.setHealthDataServiceApiStatusError(
467                                 healthConnectException.getErrorCode());
468                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
469                         tryAndThrowException(
470                                 callback,
471                                 healthConnectException,
472                                 healthConnectException.getErrorCode());
473                     } catch (Exception e) {
474                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
475                         Slog.e(TAG, "Exception: ", e);
476                         tryAndThrowException(callback, e, ERROR_INTERNAL);
477                     } finally {
478                         builder.build().log();
479                     }
480                 },
481                 uid,
482                 holdsDataManagementPermission);
483     }
484 
485     /**
486      * Read records {@code recordsParcel} from HealthConnect database.
487      *
488      * @param request ReadRecordsRequestParcel is parcel for the request object containing {@link
489      *     RecordIdFiltersParcel}.
490      * @param callback Callback to receive result of performing this operation. The records are
491      *     returned in {@link RecordsParcel} . In case of an error or a permission failure the
492      *     HealthConnect service, {@link IReadRecordsResponseCallback#onError} will be invoked with
493      *     a {@link HealthConnectExceptionParcel}.
494      */
495     @Override
readRecords( @onNull AttributionSource attributionSource, @NonNull ReadRecordsRequestParcel request, @NonNull IReadRecordsResponseCallback callback)496     public void readRecords(
497             @NonNull AttributionSource attributionSource,
498             @NonNull ReadRecordsRequestParcel request,
499             @NonNull IReadRecordsResponseCallback callback) {
500         final int uid = Binder.getCallingUid();
501         final int pid = Binder.getCallingPid();
502         final UserHandle userHandle = Binder.getCallingUserHandle();
503         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
504         final HealthConnectServiceLogger.Builder builder =
505                 new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, READ_DATA)
506                         .setPackageName(attributionSource.getPackageName());
507 
508         HealthConnectThreadScheduler.schedule(
509                 mContext,
510                 () -> {
511                     try {
512                         enforceIsForegroundUser(userHandle);
513                         verifyPackageNameFromUid(uid, attributionSource);
514                         throwExceptionIfDataSyncInProgress();
515                         AtomicBoolean enforceSelfRead = new AtomicBoolean();
516                         if (!holdsDataManagementPermission) {
517                             boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
518                             // If requesting app has only write permission allowed but no read
519                             // permission for the record type or if app is not in foreground then
520                             // allow to read its own records.
521                             enforceSelfRead.set(
522                                     mDataPermissionEnforcer.enforceReadAccessAndGetEnforceSelfRead(
523                                                     request.getRecordType(), attributionSource)
524                                             || !isInForeground);
525                             if (Constants.DEBUG) {
526                                 Slog.d(
527                                         TAG,
528                                         "Enforce self read for package "
529                                                 + attributionSource.getPackageName()
530                                                 + ":"
531                                                 + enforceSelfRead.get());
532                             }
533                             tryAcquireApiCallQuota(
534                                     uid,
535                                     QuotaCategory.QUOTA_CATEGORY_READ,
536                                     isInForeground,
537                                     builder);
538                         }
539                         final Map<String, Boolean> extraReadPermsToGrantState =
540                                 Collections.unmodifiableMap(
541                                         mDataPermissionEnforcer
542                                                 .collectExtraReadPermissionToStateMapping(
543                                                         request.getRecordType(),
544                                                         attributionSource));
545 
546                         Trace.traceBegin(TRACE_TAG_READ, TAG_READ);
547                         try {
548                             long startDateAccess = request.getStartTime();
549                             if (!holdsDataManagementPermission) {
550                                 Instant startInstant =
551                                         mPermissionHelper.getHealthDataStartDateAccess(
552                                                 attributionSource.getPackageName(), userHandle);
553                                 if (startInstant == null) {
554                                     throwExceptionIncorrectPermissionState();
555                                 }
556 
557                                 // Always set the startDateAccess for local time filter, as for
558                                 // local date time we use it in conjunction with the time filter
559                                 // start-time
560                                 if (request.usesLocalTimeFilter()
561                                         || startInstant.toEpochMilli() > startDateAccess) {
562                                     startDateAccess = startInstant.toEpochMilli();
563                                 }
564                             }
565                             Pair<List<RecordInternal<?>>, Long> readRecordsResponse =
566                                     mTransactionManager.readRecordsAndGetNextToken(
567                                             new ReadTransactionRequest(
568                                                     attributionSource.getPackageName(),
569                                                     request,
570                                                     startDateAccess,
571                                                     enforceSelfRead.get(),
572                                                     extraReadPermsToGrantState));
573                             builder.setNumberOfRecords(readRecordsResponse.first.size());
574                             long pageToken =
575                                     request.getRecordIdFiltersParcel() == null
576                                             ? readRecordsResponse.second
577                                             : DEFAULT_LONG;
578                             if (pageToken != DEFAULT_LONG) {
579                                 // pagetoken is used here to store sorting order of the result.
580                                 // An even pagetoken indicate ascending and Odd page token indicate
581                                 // descending sort order. This detail from page token will be used
582                                 // in next read request to have same sort order.
583                                 pageToken =
584                                         request.isAscending() ? pageToken * 2 : pageToken * 2 + 1;
585                             }
586 
587                             if (Constants.DEBUG) {
588                                 Slog.d(TAG, "pageToken: " + pageToken);
589                             }
590 
591                             final String packageName = attributionSource.getPackageName();
592                             final List<Integer> recordTypes =
593                                     Collections.singletonList(request.getRecordType());
594                             // Calls from controller APK should not be recorded in access logs
595                             // If an app is reading only its own data then it is not recorded in
596                             // access logs.
597                             boolean requiresLogging =
598                                     !holdsDataManagementPermission && !enforceSelfRead.get();
599                             if (requiresLogging) {
600                                 Trace.traceBegin(
601                                         TRACE_TAG_READ_SUBTASKS, TAG_READ.concat("AddAccessLog"));
602                                 AccessLogsHelper.getInstance()
603                                         .addAccessLog(packageName, recordTypes, READ);
604                                 Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
605                             }
606                             callback.onResult(
607                                     new ReadRecordsResponseParcel(
608                                             new RecordsParcel(readRecordsResponse.first),
609                                             pageToken));
610                             finishDataDeliveryRead(request.getRecordType(), attributionSource);
611                             if (requiresLogging) {
612                                 logRecordTypeSpecificReadMetrics(
613                                         readRecordsResponse.first, packageName);
614                             }
615                             builder.setDataTypesFromRecordInternals(readRecordsResponse.first)
616                                     .setHealthDataServiceApiStatusSuccess();
617                         } catch (TypeNotPresentException exception) {
618                             // All the requested package names are not present, so simply
619                             // return an empty list
620                             if (ReadTransactionRequest.TYPE_NOT_PRESENT_PACKAGE_NAME.equals(
621                                     exception.typeName())) {
622                                 if (Constants.DEBUG) {
623                                     Slog.d(
624                                             TAG,
625                                             "No app info recorded for "
626                                                     + attributionSource.getPackageName());
627                                 }
628                                 callback.onResult(
629                                         new ReadRecordsResponseParcel(
630                                                 new RecordsParcel(new ArrayList<>()),
631                                                 DEFAULT_LONG));
632                                 builder.setHealthDataServiceApiStatusSuccess();
633                             } else {
634                                 builder.setHealthDataServiceApiStatusError(
635                                         HealthConnectException.ERROR_UNKNOWN);
636                                 throw exception;
637                             }
638                         }
639                     } catch (SQLiteException sqLiteException) {
640                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
641                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
642                         tryAndThrowException(
643                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
644                     } catch (SecurityException securityException) {
645                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
646                         Slog.e(TAG, "SecurityException: ", securityException);
647                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
648                     } catch (IllegalStateException illegalStateException) {
649                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
650                         Slog.e(TAG, "IllegalStateException: ", illegalStateException);
651                         tryAndThrowException(callback, illegalStateException, ERROR_INTERNAL);
652                     } catch (HealthConnectException healthConnectException) {
653                         builder.setHealthDataServiceApiStatusError(
654                                 healthConnectException.getErrorCode());
655                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
656                         tryAndThrowException(
657                                 callback,
658                                 healthConnectException,
659                                 healthConnectException.getErrorCode());
660                     } catch (Exception e) {
661                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
662                         Slog.e(TAG, "Exception: ", e);
663                         tryAndThrowException(callback, e, ERROR_INTERNAL);
664                     } finally {
665                         Trace.traceEnd(TRACE_TAG_READ);
666                         builder.build().log();
667                     }
668                 },
669                 uid,
670                 holdsDataManagementPermission);
671     }
672 
673     /**
674      * Updates {@code recordsParcel} into the HealthConnect database.
675      *
676      * @param recordsParcel parcel for list of records to be updated.
677      * @param callback Callback to receive result of performing this operation. In case of an error
678      *     or a permission failure the HealthConnect service, {@link IEmptyResponseCallback#onError}
679      *     will be invoked with a {@link HealthConnectException}.
680      */
681     @Override
updateRecords( @onNull AttributionSource attributionSource, @NonNull RecordsParcel recordsParcel, @NonNull IEmptyResponseCallback callback)682     public void updateRecords(
683             @NonNull AttributionSource attributionSource,
684             @NonNull RecordsParcel recordsParcel,
685             @NonNull IEmptyResponseCallback callback) {
686         final int uid = Binder.getCallingUid();
687         final int pid = Binder.getCallingPid();
688         final UserHandle userHandle = Binder.getCallingUserHandle();
689         final HealthConnectServiceLogger.Builder builder =
690                 new HealthConnectServiceLogger.Builder(false, UPDATE_DATA)
691                         .setPackageName(attributionSource.getPackageName());
692         HealthConnectThreadScheduler.schedule(
693                 mContext,
694                 () -> {
695                     try {
696                         enforceIsForegroundUser(userHandle);
697                         verifyPackageNameFromUid(uid, attributionSource);
698                         if (hasDataManagementPermission(uid, pid)) {
699                             throw new SecurityException(
700                                     "Apps with android.permission.MANAGE_HEALTH_DATA permission are"
701                                             + " not allowed to insert records");
702                         }
703                         enforceMemoryRateLimit(
704                                 recordsParcel.getRecordsSize(),
705                                 recordsParcel.getRecordsChunkSize());
706                         final List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
707                         builder.setNumberOfRecords(recordInternals.size());
708                         throwExceptionIfDataSyncInProgress();
709                         mDataPermissionEnforcer.enforceRecordsWritePermissions(
710                                 recordInternals, attributionSource);
711                         boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
712                         tryAcquireApiCallQuota(
713                                 uid, QuotaCategory.QUOTA_CATEGORY_WRITE, isInForeground, builder);
714                         UpsertTransactionRequest request =
715                                 new UpsertTransactionRequest(
716                                         attributionSource.getPackageName(),
717                                         recordInternals,
718                                         mContext,
719                                         /* isInsertRequest */ false,
720                                         mDataPermissionEnforcer
721                                                 .collectExtraWritePermissionStateMapping(
722                                                         recordInternals, attributionSource));
723                         mTransactionManager.updateAll(request);
724                         tryAndReturnResult(callback, builder);
725                         finishDataDeliveryWriteRecords(recordInternals, attributionSource);
726                         logRecordTypeSpecificUpsertMetrics(
727                                 recordInternals, attributionSource.getPackageName());
728                         builder.setDataTypesFromRecordInternals(recordInternals);
729                         // Update activity dates table
730                         HealthConnectThreadScheduler.scheduleInternalTask(
731                                 () ->
732                                         ActivityDateHelper.getInstance()
733                                                 .reSyncByRecordTypeIds(
734                                                         recordInternals.stream()
735                                                                 .map(RecordInternal::getRecordType)
736                                                                 .toList()));
737                     } catch (SecurityException securityException) {
738                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
739                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
740                     } catch (SQLiteException sqLiteException) {
741                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
742                         Slog.e(TAG, "SqlException: ", sqLiteException);
743                         tryAndThrowException(
744                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
745                     } catch (IllegalArgumentException illegalArgumentException) {
746                         builder.setHealthDataServiceApiStatusError(
747                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
748 
749                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
750                         tryAndThrowException(
751                                 callback,
752                                 illegalArgumentException,
753                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
754                     } catch (HealthConnectException healthConnectException) {
755                         builder.setHealthDataServiceApiStatusError(
756                                 healthConnectException.getErrorCode());
757                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
758                         tryAndThrowException(
759                                 callback,
760                                 healthConnectException,
761                                 healthConnectException.getErrorCode());
762                     } catch (Exception e) {
763                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
764 
765                         Slog.e(TAG, "Exception: ", e);
766                         tryAndThrowException(callback, e, ERROR_INTERNAL);
767                     } finally {
768                         builder.build().log();
769                     }
770                 },
771                 uid,
772                 false);
773     }
774 
775     /**
776      * @see HealthConnectManager#getChangeLogToken
777      */
778     @Override
getChangeLogToken( @onNull AttributionSource attributionSource, @NonNull ChangeLogTokenRequest request, @NonNull IGetChangeLogTokenCallback callback)779     public void getChangeLogToken(
780             @NonNull AttributionSource attributionSource,
781             @NonNull ChangeLogTokenRequest request,
782             @NonNull IGetChangeLogTokenCallback callback) {
783         final int uid = Binder.getCallingUid();
784         final UserHandle userHandle = Binder.getCallingUserHandle();
785         final HealthConnectServiceLogger.Builder builder =
786                 new HealthConnectServiceLogger.Builder(false, GET_CHANGES_TOKEN)
787                         .setPackageName(attributionSource.getPackageName());
788         HealthConnectThreadScheduler.schedule(
789                 mContext,
790                 () -> {
791                     try {
792                         enforceIsForegroundUser(userHandle);
793                         verifyPackageNameFromUid(uid, attributionSource);
794                         tryAcquireApiCallQuota(
795                                 uid,
796                                 QuotaCategory.QUOTA_CATEGORY_READ,
797                                 mAppOpsManagerLocal.isUidInForeground(uid),
798                                 builder);
799                         throwExceptionIfDataSyncInProgress();
800                         mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
801                                 request.getRecordTypesList(), attributionSource);
802                         callback.onResult(
803                                 new ChangeLogTokenResponse(
804                                         ChangeLogsRequestHelper.getInstance()
805                                                 .getToken(
806                                                         attributionSource.getPackageName(),
807                                                         request)));
808                         builder.setHealthDataServiceApiStatusSuccess();
809                     } catch (SQLiteException sqLiteException) {
810                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
811                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
812                         tryAndThrowException(
813                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
814                     } catch (SecurityException securityException) {
815                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
816                         Slog.e(TAG, "SecurityException: ", securityException);
817                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
818                     } catch (HealthConnectException healthConnectException) {
819                         builder.setHealthDataServiceApiStatusError(
820                                 healthConnectException.getErrorCode());
821                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
822                         tryAndThrowException(
823                                 callback,
824                                 healthConnectException,
825                                 healthConnectException.getErrorCode());
826                     } catch (Exception e) {
827                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
828                         tryAndThrowException(callback, e, ERROR_INTERNAL);
829                     }
830                     {
831                         builder.build().log();
832                     }
833                 },
834                 uid,
835                 false);
836     }
837 
838     /**
839      * @hide
840      * @see HealthConnectManager#getChangeLogs
841      */
842     @Override
getChangeLogs( @onNull AttributionSource attributionSource, @NonNull ChangeLogsRequest token, IChangeLogsResponseCallback callback)843     public void getChangeLogs(
844             @NonNull AttributionSource attributionSource,
845             @NonNull ChangeLogsRequest token,
846             IChangeLogsResponseCallback callback) {
847         final int uid = Binder.getCallingUid();
848         final UserHandle userHandle = Binder.getCallingUserHandle();
849         final HealthConnectServiceLogger.Builder builder =
850                 new HealthConnectServiceLogger.Builder(false, GET_CHANGES)
851                         .setPackageName(attributionSource.getPackageName());
852 
853         HealthConnectThreadScheduler.schedule(
854                 mContext,
855                 () -> {
856                     try {
857                         enforceIsForegroundUser(userHandle);
858                         verifyPackageNameFromUid(uid, attributionSource);
859                         throwExceptionIfDataSyncInProgress();
860                         ChangeLogsRequestHelper.TokenRequest changeLogsTokenRequest =
861                                 ChangeLogsRequestHelper.getRequest(
862                                         attributionSource.getPackageName(), token.getToken());
863                         mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
864                                 changeLogsTokenRequest.getRecordTypes(), attributionSource);
865                         boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
866                         if (!isInForeground) {
867                             throwSecurityException(
868                                     attributionSource.getPackageName()
869                                             + " must be in foreground to read the change logs");
870                         }
871                         tryAcquireApiCallQuota(
872                                 uid, QuotaCategory.QUOTA_CATEGORY_READ, isInForeground, builder);
873                         Instant startDateInstant =
874                                 mPermissionHelper.getHealthDataStartDateAccess(
875                                         attributionSource.getPackageName(), userHandle);
876                         if (startDateInstant == null) {
877                             throwExceptionIncorrectPermissionState();
878                         }
879                         long startDateAccess = startDateInstant.toEpochMilli();
880                         final ChangeLogsHelper.ChangeLogsResponse changeLogsResponse =
881                                 ChangeLogsHelper.getInstance()
882                                         .getChangeLogs(changeLogsTokenRequest, token);
883 
884                         List<RecordInternal<?>> recordInternals =
885                                 mTransactionManager.readRecords(
886                                         new ReadTransactionRequest(
887                                                 ChangeLogsHelper.getRecordTypeToInsertedUuids(
888                                                         changeLogsResponse.getChangeLogsMap()),
889                                                 startDateAccess));
890                         List<DeletedLog> deletedLogs =
891                                 ChangeLogsHelper.getDeletedLogs(
892                                         changeLogsResponse.getChangeLogsMap());
893 
894                         callback.onResult(
895                                 new ChangeLogsResponse(
896                                         new RecordsParcel(recordInternals),
897                                         deletedLogs,
898                                         changeLogsResponse.getNextPageToken(),
899                                         changeLogsResponse.hasMorePages()));
900                         finishDataDeliveryRead(
901                                 changeLogsTokenRequest.getRecordTypes(), attributionSource);
902                         builder.setHealthDataServiceApiStatusSuccess()
903                                 .setNumberOfRecords(recordInternals.size() + deletedLogs.size())
904                                 .setDataTypesFromRecordInternals(recordInternals);
905                     } catch (IllegalArgumentException illegalArgumentException) {
906                         builder.setHealthDataServiceApiStatusError(
907                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
908                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
909                         tryAndThrowException(
910                                 callback,
911                                 illegalArgumentException,
912                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
913                     } catch (SQLiteException sqLiteException) {
914                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
915                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
916                         tryAndThrowException(
917                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
918                     } catch (SecurityException securityException) {
919                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
920                         Slog.e(TAG, "SecurityException: ", securityException);
921                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
922                     } catch (IllegalStateException illegalStateException) {
923                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
924                         Slog.e(TAG, "IllegalStateException: ", illegalStateException);
925                         tryAndThrowException(callback, illegalStateException, ERROR_INTERNAL);
926                     } catch (HealthConnectException healthConnectException) {
927                         builder.setHealthDataServiceApiStatusError(
928                                 healthConnectException.getErrorCode());
929                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
930                         tryAndThrowException(
931                                 callback,
932                                 healthConnectException,
933                                 healthConnectException.getErrorCode());
934                     } catch (Exception exception) {
935                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
936                         Slog.e(TAG, "Exception: ", exception);
937                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
938                     } finally {
939                         builder.build().log();
940                     }
941                 },
942                 uid,
943                 false);
944     }
945 
946     /**
947      * API to delete records based on {@code request}
948      *
949      * <p>NOTE: Though internally we only need a single API to handle deletes as SDK code transform
950      * all its delete requests to {@link DeleteUsingFiltersRequestParcel}, we have this separation
951      * to make sure no non-controller APIs can use {@link
952      * HealthConnectServiceImpl#deleteUsingFilters} API
953      */
954     @Override
deleteUsingFiltersForSelf( @onNull AttributionSource attributionSource, @NonNull DeleteUsingFiltersRequestParcel request, @NonNull IEmptyResponseCallback callback)955     public void deleteUsingFiltersForSelf(
956             @NonNull AttributionSource attributionSource,
957             @NonNull DeleteUsingFiltersRequestParcel request,
958             @NonNull IEmptyResponseCallback callback) {
959         final int uid = Binder.getCallingUid();
960         final int pid = Binder.getCallingPid();
961         final UserHandle userHandle = Binder.getCallingUserHandle();
962         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
963         final HealthConnectServiceLogger.Builder builder =
964                 new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, DELETE_DATA)
965                         .setPackageName(attributionSource.getPackageName());
966 
967         HealthConnectThreadScheduler.schedule(
968                 mContext,
969                 () -> {
970                     try {
971                         enforceIsForegroundUser(userHandle);
972                         verifyPackageNameFromUid(uid, attributionSource);
973                         throwExceptionIfDataSyncInProgress();
974                         List<Integer> recordTypeIdsToDelete =
975                                 (!request.getRecordTypeFilters().isEmpty())
976                                         ? request.getRecordTypeFilters()
977                                         : new ArrayList<>(
978                                                 RecordMapper.getInstance()
979                                                         .getRecordIdToExternalRecordClassMap()
980                                                         .keySet());
981                         // Requests from non controller apps are not allowed to use non-id
982                         // filters
983                         request.setPackageNameFilters(
984                                 Collections.singletonList(attributionSource.getPackageName()));
985 
986                         if (!holdsDataManagementPermission) {
987                             mDataPermissionEnforcer.enforceRecordIdsWritePermissions(
988                                     recordTypeIdsToDelete, attributionSource);
989                             tryAcquireApiCallQuota(
990                                     uid,
991                                     QuotaCategory.QUOTA_CATEGORY_WRITE,
992                                     mAppOpsManagerLocal.isUidInForeground(uid),
993                                     builder);
994                         }
995 
996                         deleteUsingFiltersInternal(
997                                 attributionSource,
998                                 request,
999                                 callback,
1000                                 builder,
1001                                 recordTypeIdsToDelete,
1002                                 uid,
1003                                 pid);
1004                     } catch (SQLiteException sqLiteException) {
1005                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
1006                         tryAndThrowException(
1007                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1008                     } catch (IllegalArgumentException illegalArgumentException) {
1009                         builder.setHealthDataServiceApiStatusError(
1010                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
1011                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
1012                         tryAndThrowException(
1013                                 callback,
1014                                 illegalArgumentException,
1015                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
1016                     } catch (SecurityException securityException) {
1017                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
1018                         Slog.e(TAG, "SecurityException: ", securityException);
1019                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1020                     } catch (HealthConnectException healthConnectException) {
1021                         builder.setHealthDataServiceApiStatusError(
1022                                 healthConnectException.getErrorCode());
1023                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1024                         tryAndThrowException(
1025                                 callback,
1026                                 healthConnectException,
1027                                 healthConnectException.getErrorCode());
1028                     } catch (Exception exception) {
1029                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
1030                         Slog.e(TAG, "Exception: ", exception);
1031                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1032                     } finally {
1033                         builder.build().log();
1034                     }
1035                 },
1036                 uid,
1037                 holdsDataManagementPermission);
1038     }
1039 
1040     /**
1041      * API to delete records based on {@code request}
1042      *
1043      * <p>NOTE: Though internally we only need a single API to handle deletes as SDK code transform
1044      * all its delete requests to {@link DeleteUsingFiltersRequestParcel}, we have this separation
1045      * to make sure no non-controller APIs can use this API
1046      */
1047     @Override
deleteUsingFilters( @onNull AttributionSource attributionSource, @NonNull DeleteUsingFiltersRequestParcel request, @NonNull IEmptyResponseCallback callback)1048     public void deleteUsingFilters(
1049             @NonNull AttributionSource attributionSource,
1050             @NonNull DeleteUsingFiltersRequestParcel request,
1051             @NonNull IEmptyResponseCallback callback) {
1052         final int uid = Binder.getCallingUid();
1053         final int pid = Binder.getCallingPid();
1054         final UserHandle userHandle = Binder.getCallingUserHandle();
1055         final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
1056         final HealthConnectServiceLogger.Builder builder =
1057                 new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, DELETE_DATA)
1058                         .setPackageName(attributionSource.getPackageName());
1059 
1060         HealthConnectThreadScheduler.schedule(
1061                 mContext,
1062                 () -> {
1063                     try {
1064                         enforceIsForegroundUser(userHandle);
1065                         verifyPackageNameFromUid(uid, attributionSource);
1066                         throwExceptionIfDataSyncInProgress();
1067                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1068                         List<Integer> recordTypeIdsToDelete =
1069                                 (!request.getRecordTypeFilters().isEmpty())
1070                                         ? request.getRecordTypeFilters()
1071                                         : new ArrayList<>(
1072                                                 RecordMapper.getInstance()
1073                                                         .getRecordIdToExternalRecordClassMap()
1074                                                         .keySet());
1075 
1076                         deleteUsingFiltersInternal(
1077                                 attributionSource,
1078                                 request,
1079                                 callback,
1080                                 builder,
1081                                 recordTypeIdsToDelete,
1082                                 uid,
1083                                 pid);
1084                     } catch (SQLiteException sqLiteException) {
1085                         builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
1086                         tryAndThrowException(
1087                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1088                     } catch (IllegalArgumentException illegalArgumentException) {
1089                         builder.setHealthDataServiceApiStatusError(
1090                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
1091                         Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
1092                         tryAndThrowException(
1093                                 callback,
1094                                 illegalArgumentException,
1095                                 HealthConnectException.ERROR_INVALID_ARGUMENT);
1096                     } catch (SecurityException securityException) {
1097                         builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
1098                         Slog.e(TAG, "SecurityException: ", securityException);
1099                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1100                     } catch (HealthConnectException healthConnectException) {
1101                         builder.setHealthDataServiceApiStatusError(
1102                                 healthConnectException.getErrorCode());
1103                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1104                         tryAndThrowException(
1105                                 callback,
1106                                 healthConnectException,
1107                                 healthConnectException.getErrorCode());
1108                     } catch (Exception exception) {
1109                         builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
1110                         Slog.e(TAG, "Exception: ", exception);
1111                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1112                     } finally {
1113                         builder.build().log();
1114                     }
1115                 },
1116                 uid,
1117                 holdsDataManagementPermission);
1118     }
1119 
deleteUsingFiltersInternal( @onNull AttributionSource attributionSource, @NonNull DeleteUsingFiltersRequestParcel request, @NonNull IEmptyResponseCallback callback, @NonNull HealthConnectServiceLogger.Builder builder, List<Integer> recordTypeIdsToDelete, int uid, int pid)1120     private void deleteUsingFiltersInternal(
1121             @NonNull AttributionSource attributionSource,
1122             @NonNull DeleteUsingFiltersRequestParcel request,
1123             @NonNull IEmptyResponseCallback callback,
1124             @NonNull HealthConnectServiceLogger.Builder builder,
1125             List<Integer> recordTypeIdsToDelete,
1126             int uid,
1127             int pid) {
1128         if (request.usesIdFilters() && request.usesNonIdFilters()) {
1129             throw new IllegalArgumentException(
1130                     "Requests with both id and non-id filters are not" + " supported");
1131         }
1132         int numberOfRecordsDeleted =
1133                 mTransactionManager.deleteAll(
1134                         new DeleteTransactionRequest(attributionSource.getPackageName(), request)
1135                                 .setHasManageHealthDataPermission(
1136                                         hasDataManagementPermission(uid, pid)));
1137         tryAndReturnResult(callback, builder);
1138         finishDataDeliveryWrite(recordTypeIdsToDelete, attributionSource);
1139         HealthConnectThreadScheduler.scheduleInternalTask(
1140                 () -> postDeleteTasks(recordTypeIdsToDelete));
1141 
1142         builder.setNumberOfRecords(numberOfRecordsDeleted)
1143                 .setDataTypesFromRecordTypes(recordTypeIdsToDelete);
1144     }
1145 
1146     /** API to get Priority for {@code dataCategory} */
1147     @Override
getCurrentPriority( @onNull String packageName, @HealthDataCategory.Type int dataCategory, @NonNull IGetPriorityResponseCallback callback)1148     public void getCurrentPriority(
1149             @NonNull String packageName,
1150             @HealthDataCategory.Type int dataCategory,
1151             @NonNull IGetPriorityResponseCallback callback) {
1152         final int uid = Binder.getCallingUid();
1153         final int pid = Binder.getCallingPid();
1154         final UserHandle userHandle = Binder.getCallingUserHandle();
1155         HealthConnectThreadScheduler.scheduleControllerTask(
1156                 () -> {
1157                     try {
1158                         enforceIsForegroundUser(userHandle);
1159                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1160                         throwExceptionIfDataSyncInProgress();
1161                         List<DataOrigin> dataOriginInPriorityOrder =
1162                                 HealthDataCategoryPriorityHelper.getInstance()
1163                                         .getPriorityOrder(dataCategory)
1164                                         .stream()
1165                                         .map(
1166                                                 (name) ->
1167                                                         new DataOrigin.Builder()
1168                                                                 .setPackageName(name)
1169                                                                 .build())
1170                                         .collect(Collectors.toList());
1171                         callback.onResult(
1172                                 new GetPriorityResponseParcel(
1173                                         new FetchDataOriginsPriorityOrderResponse(
1174                                                 dataOriginInPriorityOrder)));
1175                     } catch (SQLiteException sqLiteException) {
1176                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
1177                         tryAndThrowException(
1178                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1179                     } catch (SecurityException securityException) {
1180                         Slog.e(TAG, "SecurityException: ", securityException);
1181                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1182                     } catch (HealthConnectException healthConnectException) {
1183                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1184                         tryAndThrowException(
1185                                 callback,
1186                                 healthConnectException,
1187                                 healthConnectException.getErrorCode());
1188                     } catch (Exception exception) {
1189                         Slog.e(TAG, "Exception: ", exception);
1190                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1191                     }
1192                 });
1193     }
1194 
1195     /** API to update priority for permission category(ies) */
1196     @Override
updatePriority( @onNull String packageName, @NonNull UpdatePriorityRequestParcel updatePriorityRequest, @NonNull IEmptyResponseCallback callback)1197     public void updatePriority(
1198             @NonNull String packageName,
1199             @NonNull UpdatePriorityRequestParcel updatePriorityRequest,
1200             @NonNull IEmptyResponseCallback callback) {
1201         final int uid = Binder.getCallingUid();
1202         final int pid = Binder.getCallingPid();
1203         final UserHandle userHandle = Binder.getCallingUserHandle();
1204         HealthConnectThreadScheduler.scheduleControllerTask(
1205                 () -> {
1206                     try {
1207                         enforceIsForegroundUser(userHandle);
1208                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1209                         throwExceptionIfDataSyncInProgress();
1210                         HealthDataCategoryPriorityHelper.getInstance()
1211                                 .setPriorityOrder(
1212                                         updatePriorityRequest.getDataCategory(),
1213                                         updatePriorityRequest.getPackagePriorityOrder());
1214                         callback.onResult();
1215                     } catch (SQLiteException sqLiteException) {
1216                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
1217                         tryAndThrowException(
1218                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1219                     } catch (SecurityException securityException) {
1220                         Slog.e(TAG, "SecurityException: ", securityException);
1221                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1222                     } catch (HealthConnectException healthConnectException) {
1223                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1224                         tryAndThrowException(
1225                                 callback,
1226                                 healthConnectException,
1227                                 healthConnectException.getErrorCode());
1228                     } catch (Exception exception) {
1229                         Slog.e(TAG, "Exception: ", exception);
1230                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1231                     }
1232                 });
1233     }
1234 
1235     @Override
setRecordRetentionPeriodInDays( int days, @NonNull UserHandle user, IEmptyResponseCallback callback)1236     public void setRecordRetentionPeriodInDays(
1237             int days, @NonNull UserHandle user, IEmptyResponseCallback callback) {
1238         final int uid = Binder.getCallingUid();
1239         final int pid = Binder.getCallingPid();
1240         final UserHandle userHandle = Binder.getCallingUserHandle();
1241         HealthConnectThreadScheduler.scheduleControllerTask(
1242                 () -> {
1243                     try {
1244                         enforceIsForegroundUser(userHandle);
1245                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1246                         throwExceptionIfDataSyncInProgress();
1247                         AutoDeleteService.setRecordRetentionPeriodInDays(days);
1248                         callback.onResult();
1249                     } catch (SQLiteException sqLiteException) {
1250                         Slog.e(TAG, "SQLiteException: ", sqLiteException);
1251                         tryAndThrowException(
1252                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1253                     } catch (SecurityException securityException) {
1254                         Slog.e(TAG, "SecurityException: ", securityException);
1255                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1256                     } catch (HealthConnectException healthConnectException) {
1257                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1258                         tryAndThrowException(
1259                                 callback,
1260                                 healthConnectException,
1261                                 healthConnectException.getErrorCode());
1262                     } catch (Exception exception) {
1263                         Slog.e(TAG, "Exception: ", exception);
1264                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1265                     }
1266                 });
1267     }
1268 
1269     @Override
getRecordRetentionPeriodInDays(@onNull UserHandle user)1270     public int getRecordRetentionPeriodInDays(@NonNull UserHandle user) {
1271         enforceIsForegroundUser(getCallingUserHandle());
1272         throwExceptionIfDataSyncInProgress();
1273         try {
1274             mContext.enforceCallingPermission(MANAGE_HEALTH_DATA_PERMISSION, null);
1275             return AutoDeleteService.getRecordRetentionPeriodInDays();
1276         } catch (Exception e) {
1277             if (e instanceof SecurityException) {
1278                 throw e;
1279             }
1280             Slog.e(TAG, "Unable to get record retention period for " + user);
1281         }
1282 
1283         throw new RuntimeException();
1284     }
1285 
1286     /**
1287      * Returns information, represented by {@code ApplicationInfoResponse}, for all the packages
1288      * that have contributed to the health connect DB.
1289      *
1290      * @param callback Callback to receive result of performing this operation. In case of an error
1291      *     or a permission failure the HealthConnect service, {@link IEmptyResponseCallback#onError}
1292      *     will be invoked with a {@link HealthConnectException}.
1293      */
1294     @Override
getContributorApplicationsInfo(@onNull IApplicationInfoResponseCallback callback)1295     public void getContributorApplicationsInfo(@NonNull IApplicationInfoResponseCallback callback) {
1296         final int uid = Binder.getCallingUid();
1297         final int pid = Binder.getCallingPid();
1298         final UserHandle userHandle = Binder.getCallingUserHandle();
1299         HealthConnectThreadScheduler.scheduleControllerTask(
1300                 () -> {
1301                     try {
1302                         enforceIsForegroundUser(userHandle);
1303                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1304                         throwExceptionIfDataSyncInProgress();
1305                         List<AppInfo> applicationInfos =
1306                                 AppInfoHelper.getInstance().getApplicationInfosWithRecordTypes();
1307 
1308                         callback.onResult(new ApplicationInfoResponseParcel(applicationInfos));
1309                     } catch (SQLiteException sqLiteException) {
1310                         Slog.e(TAG, "SqlException: ", sqLiteException);
1311                         tryAndThrowException(
1312                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1313                     } catch (SecurityException securityException) {
1314                         Slog.e(TAG, "SecurityException: ", securityException);
1315                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1316                     } catch (HealthConnectException healthConnectException) {
1317                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1318                         tryAndThrowException(
1319                                 callback,
1320                                 healthConnectException,
1321                                 healthConnectException.getErrorCode());
1322                     } catch (Exception e) {
1323                         Slog.e(TAG, "Exception: ", e);
1324                         tryAndThrowException(callback, e, ERROR_INTERNAL);
1325                     }
1326                 });
1327     }
1328 
1329     /** Retrieves {@link RecordTypeInfoResponse} for each RecordType. */
1330     @Override
queryAllRecordTypesInfo(@onNull IRecordTypeInfoResponseCallback callback)1331     public void queryAllRecordTypesInfo(@NonNull IRecordTypeInfoResponseCallback callback) {
1332         final int uid = Binder.getCallingUid();
1333         final int pid = Binder.getCallingPid();
1334         final UserHandle userHandle = Binder.getCallingUserHandle();
1335         HealthConnectThreadScheduler.scheduleControllerTask(
1336                 () -> {
1337                     try {
1338                         enforceIsForegroundUser(userHandle);
1339                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1340                         throwExceptionIfDataSyncInProgress();
1341                         callback.onResult(
1342                                 new RecordTypeInfoResponseParcel(
1343                                         getPopulatedRecordTypeInfoResponses()));
1344                     } catch (SQLiteException sqLiteException) {
1345                         tryAndThrowException(
1346                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1347                     } catch (SecurityException securityException) {
1348                         Slog.e(TAG, "SecurityException: ", securityException);
1349                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1350                     } catch (HealthConnectException healthConnectException) {
1351                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1352                         tryAndThrowException(
1353                                 callback,
1354                                 healthConnectException,
1355                                 healthConnectException.getErrorCode());
1356                     } catch (Exception exception) {
1357                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1358                     }
1359                 });
1360     }
1361 
1362     /**
1363      * @see HealthConnectManager#queryAccessLogs
1364      */
1365     @Override
queryAccessLogs(@onNull String packageName, IAccessLogsResponseCallback callback)1366     public void queryAccessLogs(@NonNull String packageName, IAccessLogsResponseCallback callback) {
1367         final int uid = Binder.getCallingUid();
1368         final int pid = Binder.getCallingPid();
1369         final UserHandle userHandle = Binder.getCallingUserHandle();
1370 
1371         HealthConnectThreadScheduler.scheduleControllerTask(
1372                 () -> {
1373                     try {
1374                         enforceIsForegroundUser(userHandle);
1375                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1376                         throwExceptionIfDataSyncInProgress();
1377                         final List<AccessLog> accessLogsList =
1378                                 AccessLogsHelper.getInstance().queryAccessLogs();
1379                         callback.onResult(new AccessLogsResponseParcel(accessLogsList));
1380                     } catch (SecurityException securityException) {
1381                         Slog.e(TAG, "SecurityException: ", securityException);
1382                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1383                     } catch (HealthConnectException healthConnectException) {
1384                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1385                         tryAndThrowException(
1386                                 callback,
1387                                 healthConnectException,
1388                                 healthConnectException.getErrorCode());
1389                     } catch (Exception exception) {
1390                         Slog.e(TAG, "Exception: ", exception);
1391                         tryAndThrowException(callback, exception, ERROR_INTERNAL);
1392                     }
1393                 });
1394     }
1395 
1396     /**
1397      * Returns a list of unique dates for which the database has at least one entry
1398      *
1399      * @param activityDatesRequestParcel Parcel request containing records classes
1400      * @param callback Callback to receive result of performing this operation. The results are
1401      *     returned in {@link List<LocalDate>} . In case of an error or a permission failure the
1402      *     HealthConnect service, {@link IActivityDatesResponseCallback#onError} will be invoked
1403      *     with a {@link HealthConnectExceptionParcel}.
1404      */
1405     @Override
getActivityDates( @onNull ActivityDatesRequestParcel activityDatesRequestParcel, IActivityDatesResponseCallback callback)1406     public void getActivityDates(
1407             @NonNull ActivityDatesRequestParcel activityDatesRequestParcel,
1408             IActivityDatesResponseCallback callback) {
1409         final int uid = Binder.getCallingUid();
1410         final int pid = Binder.getCallingPid();
1411         final UserHandle userHandle = Binder.getCallingUserHandle();
1412 
1413         HealthConnectThreadScheduler.scheduleControllerTask(
1414                 () -> {
1415                     try {
1416                         enforceIsForegroundUser(userHandle);
1417                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1418                         throwExceptionIfDataSyncInProgress();
1419                         List<LocalDate> localDates =
1420                                 ActivityDateHelper.getInstance()
1421                                         .getActivityDates(
1422                                                 activityDatesRequestParcel.getRecordTypes());
1423 
1424                         callback.onResult(new ActivityDatesResponseParcel(localDates));
1425                     } catch (SQLiteException sqLiteException) {
1426                         Slog.e(TAG, "SqlException: ", sqLiteException);
1427                         tryAndThrowException(
1428                                 callback, sqLiteException, HealthConnectException.ERROR_IO);
1429                     } catch (SecurityException securityException) {
1430                         Slog.e(TAG, "SecurityException: ", securityException);
1431                         tryAndThrowException(callback, securityException, ERROR_SECURITY);
1432                     } catch (HealthConnectException healthConnectException) {
1433                         Slog.e(TAG, "HealthConnectException: ", healthConnectException);
1434                         tryAndThrowException(
1435                                 callback,
1436                                 healthConnectException,
1437                                 healthConnectException.getErrorCode());
1438                     } catch (Exception e) {
1439                         Slog.e(TAG, "Exception: ", e);
1440                         tryAndThrowException(callback, e, ERROR_INTERNAL);
1441                     }
1442                 });
1443     }
1444 
1445     // TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
1446     // intent.
1447     @Override
startMigration(@onNull String packageName, IMigrationCallback callback)1448     public void startMigration(@NonNull String packageName, IMigrationCallback callback) {
1449         int uid = Binder.getCallingUid();
1450         int pid = Binder.getCallingPid();
1451         final UserHandle userHandle = Binder.getCallingUserHandle();
1452 
1453         HealthConnectThreadScheduler.scheduleInternalTask(
1454                 () -> {
1455                     try {
1456                         enforceIsForegroundUser(userHandle);
1457                         mContext.enforcePermission(
1458                                 MIGRATE_HEALTH_CONNECT_DATA,
1459                                 pid,
1460                                 uid,
1461                                 "Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
1462                         enforceShowMigrationInfoIntent(packageName, uid);
1463                         mBackupRestore.runWithStatesReadLock(
1464                                 () -> {
1465                                     if (mBackupRestore.isRestoreMergingInProgress()) {
1466                                         throw new MigrationException(
1467                                                 "Cannot start data migration. Backup and restore in"
1468                                                         + " progress.",
1469                                                 MigrationException.ERROR_INTERNAL,
1470                                                 null);
1471                                     }
1472                                     mMigrationStateManager.startMigration(mContext);
1473                                 });
1474                         PriorityMigrationHelper.getInstance().populatePreMigrationPriority();
1475                         callback.onSuccess();
1476                     } catch (Exception e) {
1477                         Slog.e(TAG, "Exception: ", e);
1478                         tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
1479                     }
1480                 });
1481     }
1482 
1483     // TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
1484     // intent.
1485     @Override
finishMigration(@onNull String packageName, IMigrationCallback callback)1486     public void finishMigration(@NonNull String packageName, IMigrationCallback callback) {
1487         int uid = Binder.getCallingUid();
1488         int pid = Binder.getCallingPid();
1489         final UserHandle userHandle = Binder.getCallingUserHandle();
1490 
1491         HealthConnectThreadScheduler.scheduleInternalTask(
1492                 () -> {
1493                     try {
1494                         enforceIsForegroundUser(userHandle);
1495                         mContext.enforcePermission(
1496                                 MIGRATE_HEALTH_CONNECT_DATA,
1497                                 pid,
1498                                 uid,
1499                                 "Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
1500                         enforceShowMigrationInfoIntent(packageName, uid);
1501                         mMigrationStateManager.finishMigration(mContext);
1502                         AppInfoHelper.getInstance().syncAppInfoRecordTypesUsed();
1503                         callback.onSuccess();
1504                     } catch (Exception e) {
1505                         Slog.e(TAG, "Exception: ", e);
1506                         // TODO(b/263897830): Verify migration state and send errors properly
1507                         tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
1508                     }
1509                 });
1510     }
1511 
1512     // TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
1513     // intent.
1514     @Override
writeMigrationData( @onNull String packageName, MigrationEntityParcel parcel, IMigrationCallback callback)1515     public void writeMigrationData(
1516             @NonNull String packageName,
1517             MigrationEntityParcel parcel,
1518             IMigrationCallback callback) {
1519         int uid = Binder.getCallingUid();
1520         int pid = Binder.getCallingPid();
1521         UserHandle callingUserHandle = getCallingUserHandle();
1522 
1523         HealthConnectThreadScheduler.scheduleInternalTask(
1524                 () -> {
1525                     try {
1526                         enforceIsForegroundUser(callingUserHandle);
1527                         mContext.enforcePermission(
1528                                 MIGRATE_HEALTH_CONNECT_DATA,
1529                                 pid,
1530                                 uid,
1531                                 "Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
1532                         enforceShowMigrationInfoIntent(packageName, uid);
1533                         mMigrationStateManager.validateWriteMigrationData();
1534                         getDataMigrationManager(callingUserHandle)
1535                                 .apply(parcel.getMigrationEntities());
1536                         callback.onSuccess();
1537                     } catch (DataMigrationManager.EntityWriteException e) {
1538                         Slog.e(TAG, "Exception: ", e);
1539                         tryAndThrowException(
1540                                 callback,
1541                                 e,
1542                                 MigrationException.ERROR_MIGRATE_ENTITY,
1543                                 e.getEntityId());
1544                     } catch (Exception e) {
1545                         Slog.e(TAG, "Exception: ", e);
1546                         tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
1547                     }
1548                 });
1549     }
1550 
insertMinDataMigrationSdkExtensionVersion( @onNull String packageName, int requiredSdkExtension, IMigrationCallback callback)1551     public void insertMinDataMigrationSdkExtensionVersion(
1552             @NonNull String packageName, int requiredSdkExtension, IMigrationCallback callback) {
1553         int uid = Binder.getCallingUid();
1554         int pid = Binder.getCallingPid();
1555         final UserHandle userHandle = Binder.getCallingUserHandle();
1556 
1557         HealthConnectThreadScheduler.scheduleInternalTask(
1558                 () -> {
1559                     try {
1560                         enforceIsForegroundUser(userHandle);
1561                         mContext.enforcePermission(
1562                                 MIGRATE_HEALTH_CONNECT_DATA,
1563                                 pid,
1564                                 uid,
1565                                 "Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
1566                         enforceShowMigrationInfoIntent(packageName, uid);
1567                         mMigrationStateManager.validateSetMinSdkVersion();
1568                         mMigrationStateManager.setMinDataMigrationSdkExtensionVersion(
1569                                 mContext, requiredSdkExtension);
1570 
1571                         callback.onSuccess();
1572                     } catch (Exception e) {
1573                         Slog.e(TAG, "Exception: ", e);
1574                         tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
1575                     }
1576                 });
1577     }
1578 
1579     /**
1580      * @see HealthConnectManager#stageAllHealthConnectRemoteData
1581      */
1582     @Override
stageAllHealthConnectRemoteData( @onNull StageRemoteDataRequest stageRemoteDataRequest, @NonNull UserHandle userHandle, @NonNull IDataStagingFinishedCallback callback)1583     public void stageAllHealthConnectRemoteData(
1584             @NonNull StageRemoteDataRequest stageRemoteDataRequest,
1585             @NonNull UserHandle userHandle,
1586             @NonNull IDataStagingFinishedCallback callback) {
1587         Map<String, ParcelFileDescriptor> origPfdsByFileName =
1588                 stageRemoteDataRequest.getPfdsByFileName();
1589         Map<String, HealthConnectException> exceptionsByFileName =
1590                 new ArrayMap<>(origPfdsByFileName.size());
1591         Map<String, ParcelFileDescriptor> pfdsByFileName =
1592                 new ArrayMap<>(origPfdsByFileName.size());
1593 
1594         try {
1595             mDataPermissionEnforcer.enforceAnyOfPermissions(
1596                     Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA,
1597                     HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION);
1598 
1599             enforceIsForegroundUser(Binder.getCallingUserHandle());
1600 
1601             for (Entry<String, ParcelFileDescriptor> entry : origPfdsByFileName.entrySet()) {
1602                 try {
1603                     pfdsByFileName.put(entry.getKey(), entry.getValue().dup());
1604                 } catch (IOException e) {
1605                     Slog.e(TAG, "IOException: ", e);
1606                     exceptionsByFileName.put(
1607                             entry.getKey(),
1608                             new HealthConnectException(
1609                                     HealthConnectException.ERROR_IO, e.getMessage()));
1610                 }
1611             }
1612 
1613             HealthConnectThreadScheduler.scheduleInternalTask(
1614                     () -> {
1615                         if (!mBackupRestore.prepForStagingIfNotAlreadyDone()) {
1616                             try {
1617                                 callback.onResult();
1618                             } catch (RemoteException e) {
1619                                 Log.e(TAG, "Restore response could not be sent to the caller.", e);
1620                             }
1621                             return;
1622                         }
1623                         mBackupRestore.stageAllHealthConnectRemoteData(
1624                                 pfdsByFileName,
1625                                 exceptionsByFileName,
1626                                 userHandle.getIdentifier(),
1627                                 callback);
1628                     });
1629         } catch (SecurityException | IllegalStateException e) {
1630             Log.e(TAG, "Exception encountered while staging", e);
1631             try {
1632                 @HealthConnectException.ErrorCode int errorCode =
1633                         (e instanceof SecurityException) ? ERROR_SECURITY : ERROR_INTERNAL;
1634                 exceptionsByFileName.put("", new HealthConnectException(
1635                         errorCode,
1636                         e.getMessage()));
1637 
1638                 callback.onError(
1639                         new StageRemoteDataException(exceptionsByFileName));
1640             } catch (RemoteException remoteException) {
1641                 Log.e(TAG, "Restore permission response could not be sent to the caller.", e);
1642             }
1643         }
1644     }
1645 
1646     /**
1647      * @see HealthConnectManager#getAllDataForBackup
1648      */
1649     @Override
getAllDataForBackup( @onNull StageRemoteDataRequest stageRemoteDataRequest, @NonNull UserHandle userHandle)1650     public void getAllDataForBackup(
1651             @NonNull StageRemoteDataRequest stageRemoteDataRequest,
1652             @NonNull UserHandle userHandle) {
1653         mContext.enforceCallingPermission(HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION, null);
1654         mBackupRestore.getAllDataForBackup(stageRemoteDataRequest, userHandle);
1655     }
1656 
1657     /**
1658      * @see HealthConnectManager#getAllBackupFileNames
1659      */
1660     @Override
getAllBackupFileNames(boolean forDeviceToDevice)1661     public BackupFileNamesSet getAllBackupFileNames(boolean forDeviceToDevice) {
1662         mContext.enforceCallingPermission(HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION, null);
1663         return mBackupRestore.getAllBackupFileNames(forDeviceToDevice);
1664     }
1665 
1666     /**
1667      * @see HealthConnectManager#deleteAllStagedRemoteData
1668      */
1669     @Override
deleteAllStagedRemoteData(@onNull UserHandle userHandle)1670     public void deleteAllStagedRemoteData(@NonNull UserHandle userHandle) {
1671         mContext.enforceCallingPermission(
1672                 DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA_PERMISSION, null);
1673         mBackupRestore.deleteAndResetEverything(userHandle);
1674         mMigrationStateManager.clearCaches(mContext);
1675         AppInfoHelper.getInstance().clearData(mTransactionManager);
1676         ActivityDateHelper.getInstance().clearData(mTransactionManager);
1677         MigrationEntityHelper.getInstance().clearData(mTransactionManager);
1678         HealthDataCategoryPriorityHelper.getInstance().clearData(mTransactionManager);
1679         PriorityMigrationHelper.getInstance().clearData(mTransactionManager);
1680         RateLimiter.clearCache();
1681         String[] packageNames = mContext.getPackageManager().getPackagesForUid(getCallingUid());
1682         for (String packageName : packageNames) {
1683             mFirstGrantTimeManager.setFirstGrantTime(packageName, Instant.now(), userHandle);
1684         }
1685     }
1686 
1687     /**
1688      * @see HealthConnectManager#updateDataDownloadState
1689      */
1690     @Override
updateDataDownloadState(@ataDownloadState int downloadState)1691     public void updateDataDownloadState(@DataDownloadState int downloadState) {
1692         mContext.enforceCallingPermission(
1693                 Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA, null);
1694         enforceIsForegroundUser(getCallingUserHandle());
1695         mBackupRestore.updateDataDownloadState(downloadState);
1696     }
1697 
1698     /**
1699      * @see HealthConnectManager#getHealthConnectDataState
1700      */
1701     @Override
getHealthConnectDataState(@onNull IGetHealthConnectDataStateCallback callback)1702     public void getHealthConnectDataState(@NonNull IGetHealthConnectDataStateCallback callback) {
1703         try {
1704             mDataPermissionEnforcer.enforceAnyOfPermissions(
1705                     MANAGE_HEALTH_DATA_PERMISSION, Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
1706             final UserHandle userHandle = Binder.getCallingUserHandle();
1707             enforceIsForegroundUser(userHandle);
1708             HealthConnectThreadScheduler.scheduleInternalTask(
1709                     () -> {
1710                         try {
1711                             @HealthConnectDataState.DataRestoreError
1712                             int dataRestoreError = mBackupRestore.getDataRestoreError();
1713                             @HealthConnectDataState.DataRestoreState
1714                             int dataRestoreState = mBackupRestore.getDataRestoreState();
1715 
1716                             try {
1717                                 callback.onResult(
1718                                         new HealthConnectDataState(
1719                                                 dataRestoreState,
1720                                                 dataRestoreError,
1721                                                 mMigrationStateManager.getMigrationState()));
1722                             } catch (RemoteException remoteException) {
1723                                 Log.e(
1724                                         TAG,
1725                                         "HealthConnectDataState could not be sent to the caller.",
1726                                         remoteException);
1727                             }
1728                         } catch (RuntimeException e) {
1729                             // exception getting the state from the disk
1730                             try {
1731                                 callback.onError(
1732                                         new HealthConnectExceptionParcel(
1733                                                 new HealthConnectException(
1734                                                         HealthConnectException.ERROR_IO,
1735                                                         e.getMessage())));
1736                             } catch (RemoteException remoteException) {
1737                                 Log.e(
1738                                         TAG,
1739                                         "Exception for getHealthConnectDataState could not be sent"
1740                                                 + " to the caller.",
1741                                         remoteException);
1742                             }
1743                         }
1744                     });
1745         } catch (SecurityException | IllegalStateException e) {
1746             Log.e(TAG, "getHealthConnectDataState: Exception encountered", e);
1747             @HealthConnectException.ErrorCode int errorCode =
1748                     (e instanceof SecurityException) ? ERROR_SECURITY
1749                             : ERROR_INTERNAL;
1750             try {
1751                 callback.onError(
1752                         new HealthConnectExceptionParcel(
1753                                 new HealthConnectException(
1754                                         errorCode,
1755                                         e.getMessage())));
1756             } catch (RemoteException remoteException) {
1757                 Log.e(TAG, "getHealthConnectDataState error could not be sent", e);
1758             }
1759         }
1760     }
1761 
1762     /**
1763      * @see HealthConnectManager#getHealthConnectMigrationUiState
1764      */
1765     @Override
getHealthConnectMigrationUiState( @onNull IGetHealthConnectMigrationUiStateCallback callback)1766     public void getHealthConnectMigrationUiState(
1767             @NonNull IGetHealthConnectMigrationUiStateCallback callback) {
1768         final int uid = Binder.getCallingUid();
1769         final int pid = Binder.getCallingPid();
1770         final UserHandle userHandle = Binder.getCallingUserHandle();
1771         HealthConnectThreadScheduler.scheduleInternalTask(
1772                 () -> {
1773                     try {
1774                         enforceIsForegroundUser(userHandle);
1775                         mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
1776 
1777                         try {
1778                             callback.onResult(
1779                                     new HealthConnectMigrationUiState(
1780                                             mMigrationUiStateManager
1781                                                     .getHealthConnectMigrationUiState()));
1782                         } catch (RemoteException remoteException) {
1783                             Log.e(
1784                                     TAG,
1785                                     "HealthConnectMigrationUiState could not be sent to the"
1786                                             + " caller.",
1787                                     remoteException);
1788                         }
1789                     } catch (SecurityException securityException) {
1790                         try {
1791                             callback.onError(
1792                                     new HealthConnectExceptionParcel(
1793                                             new HealthConnectException(
1794                                                     ERROR_SECURITY,
1795                                                     securityException.getMessage())));
1796                         } catch (RemoteException remoteException) {
1797                             Log.e(
1798                                     TAG,
1799                                     "Exception for HealthConnectMigrationUiState could not be sent"
1800                                             + " to the caller.",
1801                                     remoteException);
1802                         }
1803                     } catch (RuntimeException e) {
1804                         // exception getting the state from the disk
1805                         try {
1806                             callback.onError(
1807                                     new HealthConnectExceptionParcel(
1808                                             new HealthConnectException(
1809                                                     HealthConnectException.ERROR_IO,
1810                                                     e.getMessage())));
1811                         } catch (RemoteException remoteException) {
1812                             Log.e(
1813                                     TAG,
1814                                     "Exception for HealthConnectMigrationUiState could not be sent"
1815                                             + " to the caller.",
1816                                     remoteException);
1817                         }
1818                     }
1819                 });
1820     }
1821 
1822     // Cancel BR timeouts - this might be needed when a user is going into background.
cancelBackupRestoreTimeouts()1823     void cancelBackupRestoreTimeouts() {
1824         mBackupRestore.cancelAllJobs();
1825     }
1826 
tryAcquireApiCallQuota( int uid, @QuotaCategory.Type int quotaCategory, boolean isInForeground, HealthConnectServiceLogger.Builder builder)1827     private void tryAcquireApiCallQuota(
1828             int uid,
1829             @QuotaCategory.Type int quotaCategory,
1830             boolean isInForeground,
1831             HealthConnectServiceLogger.Builder builder) {
1832         try {
1833             RateLimiter.tryAcquireApiCallQuota(uid, quotaCategory, isInForeground);
1834         } catch (RateLimiterException rateLimiterException) {
1835             builder.setRateLimit(
1836                     rateLimiterException.getRateLimiterQuotaBucket(),
1837                     rateLimiterException.getRateLimiterQuotaLimit());
1838             throw new HealthConnectException(
1839                     rateLimiterException.getErrorCode(), rateLimiterException.getMessage());
1840         }
1841     }
1842 
enforceMemoryRateLimit(List<Long> recordsSize, long recordsChunkSize)1843     private void enforceMemoryRateLimit(List<Long> recordsSize, long recordsChunkSize) {
1844         recordsSize.forEach(RateLimiter::checkMaxRecordMemoryUsage);
1845         RateLimiter.checkMaxChunkMemoryUsage(recordsChunkSize);
1846     }
1847 
enforceIsForegroundUser(UserHandle callingUserHandle)1848     private void enforceIsForegroundUser(UserHandle callingUserHandle) {
1849         if (!callingUserHandle.equals(mCurrentForegroundUser)) {
1850             throw new IllegalStateException(
1851                     "Calling user: "
1852                             + callingUserHandle.getIdentifier()
1853                             + "is not the current foreground user: "
1854                             + mCurrentForegroundUser.getIdentifier()
1855                             + ". HC request must be called"
1856                             + " from the current foreground user.");
1857         }
1858     }
1859 
isDataSyncInProgress()1860     private boolean isDataSyncInProgress() {
1861         return mMigrationStateManager.isMigrationInProgress()
1862                 || mBackupRestore.isRestoreMergingInProgress();
1863     }
1864 
1865     @VisibleForTesting
getStagedRemoteFileNames(int userId)1866     Set<String> getStagedRemoteFileNames(int userId) {
1867         return mBackupRestore.getStagedRemoteFileNames(userId);
1868     }
1869 
1870     @NonNull
getDataMigrationManager(@onNull UserHandle userHandle)1871     private DataMigrationManager getDataMigrationManager(@NonNull UserHandle userHandle) {
1872         final Context userContext = mContext.createContextAsUser(userHandle, 0);
1873 
1874         return new DataMigrationManager(
1875                 userContext,
1876                 mTransactionManager,
1877                 mPermissionHelper,
1878                 mFirstGrantTimeManager,
1879                 DeviceInfoHelper.getInstance(),
1880                 AppInfoHelper.getInstance(),
1881                 MigrationEntityHelper.getInstance(),
1882                 RecordHelperProvider.getInstance(),
1883                 HealthDataCategoryPriorityHelper.getInstance(),
1884                 PriorityMigrationHelper.getInstance(),
1885                 ActivityDateHelper.getInstance());
1886     }
1887 
enforceCallingPackageBelongsToUid(String packageName, int callingUid)1888     private void enforceCallingPackageBelongsToUid(String packageName, int callingUid) {
1889         int packageUid;
1890         try {
1891             packageUid =
1892                     mContext.getPackageManager()
1893                             .getPackageUid(
1894                                     packageName, /* flags */ PackageManager.PackageInfoFlags.of(0));
1895         } catch (PackageManager.NameNotFoundException e) {
1896             throw new IllegalStateException(packageName + " not found");
1897         }
1898         if (UserHandle.getAppId(packageUid) != UserHandle.getAppId(callingUid)) {
1899             throwSecurityException(packageName + " does not belong to uid " + callingUid);
1900         }
1901     }
1902 
1903     /**
1904      * Verify various aspects of the calling user.
1905      *
1906      * @param callingUid Uid of the caller, usually retrieved from Binder for authenticity.
1907      * @param callerAttributionSource The permission identity of the caller
1908      */
verifyPackageNameFromUid( int callingUid, @NonNull AttributionSource callerAttributionSource)1909     private void verifyPackageNameFromUid(
1910             int callingUid, @NonNull AttributionSource callerAttributionSource) {
1911         // Check does the attribution source is one for the calling app.
1912         callerAttributionSource.enforceCallingUid();
1913         // Obtain the user where the client is running in.
1914         UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid);
1915         Context callingUserContext = mContext.createContextAsUser(callingUserHandle, 0);
1916         String callingPackageName =
1917                 Objects.requireNonNull(callerAttributionSource.getPackageName());
1918         verifyCallingPackage(callingUserContext, callingUid, callingPackageName);
1919     }
1920 
1921     /**
1922      * Check that the caller's supposed package name matches the uid making the call.
1923      *
1924      * @throws SecurityException if the package name and uid don't match.
1925      */
verifyCallingPackage( @onNull Context actualCallingUserContext, int actualCallingUid, @NonNull String claimedCallingPackage)1926     private void verifyCallingPackage(
1927             @NonNull Context actualCallingUserContext,
1928             int actualCallingUid,
1929             @NonNull String claimedCallingPackage) {
1930         int claimedCallingUid = getPackageUid(actualCallingUserContext, claimedCallingPackage);
1931         if (claimedCallingUid != actualCallingUid) {
1932             throwSecurityException(
1933                     claimedCallingPackage + " does not belong to uid " + actualCallingUid);
1934         }
1935     }
1936 
1937     /** Finds the UID of the {@code packageName} in the given {@code context}. */
getPackageUid(@onNull Context context, @NonNull String packageName)1938     private int getPackageUid(@NonNull Context context, @NonNull String packageName) {
1939         try {
1940             return context.getPackageManager().getPackageUid(packageName, /* flags= */ 0);
1941         } catch (PackageManager.NameNotFoundException e) {
1942             return Process.INVALID_UID;
1943         }
1944     }
1945 
enforceShowMigrationInfoIntent(String packageName, int callingUid)1946     private void enforceShowMigrationInfoIntent(String packageName, int callingUid) {
1947         enforceCallingPackageBelongsToUid(packageName, callingUid);
1948 
1949         Intent intentToCheck =
1950                 new Intent(HealthConnectManager.ACTION_SHOW_MIGRATION_INFO).setPackage(packageName);
1951 
1952         ResolveInfo resolveResult =
1953                 mContext.getPackageManager()
1954                         .resolveActivity(
1955                                 intentToCheck,
1956                                 PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL));
1957 
1958         if (Objects.isNull(resolveResult)) {
1959             throw new IllegalArgumentException(
1960                     packageName
1961                             + " does not handle intent "
1962                             + HealthConnectManager.ACTION_SHOW_MIGRATION_INFO);
1963         }
1964     }
1965 
getPopulatedRecordTypeInfoResponses()1966     private Map<Integer, List<DataOrigin>> getPopulatedRecordTypeInfoResponses() {
1967         Map<Integer, Class<? extends Record>> recordIdToExternalRecordClassMap =
1968                 RecordMapper.getInstance().getRecordIdToExternalRecordClassMap();
1969         AppInfoHelper appInfoHelper = AppInfoHelper.getInstance();
1970         Map<Integer, List<DataOrigin>> recordTypeInfoResponses =
1971                 new ArrayMap<>(recordIdToExternalRecordClassMap.size());
1972         Map<Integer, Set<String>> recordTypeToContributingPackagesMap =
1973                 appInfoHelper.getRecordTypesToContributingPackagesMap();
1974         recordIdToExternalRecordClassMap
1975                 .keySet()
1976                 .forEach(
1977                         (recordType) -> {
1978                             if (recordTypeToContributingPackagesMap.containsKey(recordType)) {
1979                                 List<DataOrigin> packages =
1980                                         recordTypeToContributingPackagesMap.get(recordType).stream()
1981                                                 .map(
1982                                                         (packageName) ->
1983                                                                 new DataOrigin.Builder()
1984                                                                         .setPackageName(packageName)
1985                                                                         .build())
1986                                                 .toList();
1987                                 recordTypeInfoResponses.put(recordType, packages);
1988                             } else {
1989                                 recordTypeInfoResponses.put(recordType, Collections.emptyList());
1990                             }
1991                         });
1992         return recordTypeInfoResponses;
1993     }
1994 
hasDataManagementPermission(int uid, int pid)1995     private boolean hasDataManagementPermission(int uid, int pid) {
1996         return mContext.checkPermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid)
1997                 == PERMISSION_GRANTED;
1998     }
1999 
finishDataDeliveryRead(int recordTypeId, AttributionSource attributionSource)2000     private void finishDataDeliveryRead(int recordTypeId, AttributionSource attributionSource) {
2001         finishDataDeliveryRead(Collections.singletonList(recordTypeId), attributionSource);
2002     }
2003 
finishDataDeliveryRead( List<Integer> recordTypeIds, AttributionSource attributionSource)2004     private void finishDataDeliveryRead(
2005             List<Integer> recordTypeIds, AttributionSource attributionSource) {
2006         Trace.traceBegin(TRACE_TAG_READ_SUBTASKS, TAG_READ.concat("FinishDataDeliveryRead"));
2007 
2008         try {
2009             for (Integer recordTypeId : recordTypeIds) {
2010                 String permissionName =
2011                         HealthPermissions.getHealthReadPermission(
2012                                 RecordTypePermissionCategoryMapper
2013                                         .getHealthPermissionCategoryForRecordType(recordTypeId));
2014                 mPermissionManager.finishDataDelivery(permissionName, attributionSource);
2015             }
2016         } catch (Exception exception) {
2017             // Ignore: HC API has already fulfilled the result, ignore any exception we hit here
2018         }
2019         Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
2020     }
2021 
finishDataDeliveryWriteRecords( List<RecordInternal<?>> recordInternals, AttributionSource attributionSource)2022     private void finishDataDeliveryWriteRecords(
2023             List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) {
2024         Trace.traceBegin(TRACE_TAG_READ_SUBTASKS, TAG_READ.concat(".FinishDataDeliveryWrite"));
2025         Set<Integer> recordTypeIdsToEnforce = new ArraySet<>();
2026         for (RecordInternal<?> recordInternal : recordInternals) {
2027             recordTypeIdsToEnforce.add(recordInternal.getRecordType());
2028         }
2029 
2030         finishDataDeliveryWrite(recordTypeIdsToEnforce.stream().toList(), attributionSource);
2031         Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
2032     }
2033 
finishDataDeliveryWrite( List<Integer> recordTypeIds, AttributionSource attributionSource)2034     private void finishDataDeliveryWrite(
2035             List<Integer> recordTypeIds, AttributionSource attributionSource) {
2036         try {
2037             for (Integer recordTypeId : recordTypeIds) {
2038                 String permissionName =
2039                         HealthPermissions.getHealthWritePermission(
2040                                 RecordTypePermissionCategoryMapper
2041                                         .getHealthPermissionCategoryForRecordType(recordTypeId));
2042                 mPermissionManager.finishDataDelivery(permissionName, attributionSource);
2043             }
2044         } catch (Exception exception) {
2045             // Ignore: HC API has already fulfilled the result, ignore any exception we hit here
2046         }
2047     }
2048 
enforceBinderUidIsSameAsAttributionSourceUid( int binderUid, int attributionSourceUid)2049     private void enforceBinderUidIsSameAsAttributionSourceUid(
2050             int binderUid, int attributionSourceUid) {
2051         if (binderUid != attributionSourceUid) {
2052             throw new SecurityException("Binder uid must be equal to attribution source uid.");
2053         }
2054     }
2055 
throwExceptionIncorrectPermissionState()2056     private void throwExceptionIncorrectPermissionState() {
2057         throw new IllegalStateException(
2058                 "Incorrect health permission state, likely"
2059                         + " because the calling application's manifest does not specify handling "
2060                         + Intent.ACTION_VIEW_PERMISSION_USAGE
2061                         + " with "
2062                         + HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS);
2063     }
2064 
logRecordTypeSpecificUpsertMetrics( @onNull List<RecordInternal<?>> recordInternals, @NonNull String packageName)2065     private void logRecordTypeSpecificUpsertMetrics(
2066             @NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
2067         Objects.requireNonNull(recordInternals);
2068         Objects.requireNonNull(packageName);
2069 
2070         Map<Integer, List<RecordInternal<?>>> recordTypeToRecordInternals =
2071                 getRecordTypeToListOfRecords(recordInternals);
2072         for (Entry<Integer, List<RecordInternal<?>>> recordTypeToRecordInternalsEntry :
2073                 recordTypeToRecordInternals.entrySet()) {
2074             RecordHelper<?> recordHelper =
2075                     RecordHelperProvider.getInstance()
2076                             .getRecordHelper(recordTypeToRecordInternalsEntry.getKey());
2077             recordHelper.logUpsertMetrics(recordTypeToRecordInternalsEntry.getValue(), packageName);
2078         }
2079     }
2080 
logRecordTypeSpecificReadMetrics( @onNull List<RecordInternal<?>> recordInternals, @NonNull String packageName)2081     private void logRecordTypeSpecificReadMetrics(
2082             @NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
2083         Objects.requireNonNull(recordInternals);
2084         Objects.requireNonNull(packageName);
2085 
2086         Map<Integer, List<RecordInternal<?>>> recordTypeToRecordInternals =
2087                 getRecordTypeToListOfRecords(recordInternals);
2088         for (Entry<Integer, List<RecordInternal<?>>> recordTypeToRecordInternalsEntry :
2089                 recordTypeToRecordInternals.entrySet()) {
2090             RecordHelper<?> recordHelper =
2091                     RecordHelperProvider.getInstance()
2092                             .getRecordHelper(recordTypeToRecordInternalsEntry.getKey());
2093             recordHelper.logReadMetrics(recordTypeToRecordInternalsEntry.getValue(), packageName);
2094         }
2095     }
2096 
getRecordTypeToListOfRecords( List<RecordInternal<?>> recordInternals)2097     private Map<Integer, List<RecordInternal<?>>> getRecordTypeToListOfRecords(
2098             List<RecordInternal<?>> recordInternals) {
2099 
2100         return recordInternals.stream()
2101                 .collect(Collectors.groupingBy(RecordInternal::getRecordType));
2102     }
2103 
throwSecurityException(String message)2104     private void throwSecurityException(String message) {
2105         throw new SecurityException(message);
2106     }
2107 
throwExceptionIfDataSyncInProgress()2108     private void throwExceptionIfDataSyncInProgress() {
2109         if (isDataSyncInProgress()) {
2110             throw new HealthConnectException(
2111                     HealthConnectException.ERROR_DATA_SYNC_IN_PROGRESS,
2112                     "Storage data sync in progress. API calls are blocked");
2113         }
2114     }
2115 
2116     /**
2117      * Throws an IllegalState Exception if data migration or restore is in process. This is only
2118      * used by HealthConnect synchronous APIs as {@link HealthConnectException} is lost between
2119      * processes on synchronous APIs and can only be returned to the caller for the APIs with a
2120      * callback.
2121      */
throwIllegalStateExceptionIfDataSyncInProgress()2122     private void throwIllegalStateExceptionIfDataSyncInProgress() {
2123         if (isDataSyncInProgress()) {
2124             throw new IllegalStateException("Storage data sync in progress. API calls are blocked");
2125         }
2126     }
2127 
postDeleteTasks(List<Integer> recordTypeIdsToDelete)2128     private static void postDeleteTasks(List<Integer> recordTypeIdsToDelete) {
2129         Trace.traceBegin(TRACE_TAG_DELETE_SUBTASKS, TAG_INSERT.concat("PostDeleteTasks"));
2130         if (recordTypeIdsToDelete != null && !recordTypeIdsToDelete.isEmpty()) {
2131             AppInfoHelper.getInstance()
2132                     .syncAppInfoRecordTypesUsed(new HashSet<>(recordTypeIdsToDelete));
2133             ActivityDateHelper.getInstance().reSyncByRecordTypeIds(recordTypeIdsToDelete);
2134         }
2135         Trace.traceEnd(TRACE_TAG_DELETE_SUBTASKS);
2136     }
2137 
tryAndReturnResult( IEmptyResponseCallback callback, HealthConnectServiceLogger.Builder builder)2138     private static void tryAndReturnResult(
2139             IEmptyResponseCallback callback, HealthConnectServiceLogger.Builder builder) {
2140         try {
2141             callback.onResult();
2142             builder.setHealthDataServiceApiStatusSuccess();
2143         } catch (RemoteException e) {
2144             Slog.e(TAG, "Remote call failed", e);
2145             builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
2146         }
2147     }
2148 
tryAndReturnResult( IInsertRecordsResponseCallback callback, List<String> uuids, HealthConnectServiceLogger.Builder builder)2149     private static void tryAndReturnResult(
2150             IInsertRecordsResponseCallback callback,
2151             List<String> uuids,
2152             HealthConnectServiceLogger.Builder builder) {
2153         try {
2154             callback.onResult(new InsertRecordsResponseParcel(uuids));
2155             builder.setHealthDataServiceApiStatusSuccess();
2156         } catch (RemoteException e) {
2157             Slog.e(TAG, "Remote call failed", e);
2158             builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
2159         }
2160     }
2161 
tryAndThrowException( @onNull IInsertRecordsResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2162     private static void tryAndThrowException(
2163             @NonNull IInsertRecordsResponseCallback callback,
2164             @NonNull Exception exception,
2165             @HealthConnectException.ErrorCode int errorCode) {
2166         try {
2167             callback.onError(
2168                     new HealthConnectExceptionParcel(
2169                             new HealthConnectException(errorCode, exception.toString())));
2170         } catch (RemoteException e) {
2171             Log.e(TAG, "Unable to send result to the callback", e);
2172         }
2173     }
2174 
tryAndThrowException( @onNull IAggregateRecordsResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2175     private static void tryAndThrowException(
2176             @NonNull IAggregateRecordsResponseCallback callback,
2177             @NonNull Exception exception,
2178             @HealthConnectException.ErrorCode int errorCode) {
2179         try {
2180             callback.onError(
2181                     new HealthConnectExceptionParcel(
2182                             new HealthConnectException(errorCode, exception.toString())));
2183         } catch (RemoteException e) {
2184             Log.e(TAG, "Unable to send result to the callback", e);
2185         }
2186     }
2187 
tryAndThrowException( @onNull IReadRecordsResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2188     private static void tryAndThrowException(
2189             @NonNull IReadRecordsResponseCallback callback,
2190             @NonNull Exception exception,
2191             @HealthConnectException.ErrorCode int errorCode) {
2192         try {
2193             callback.onError(
2194                     new HealthConnectExceptionParcel(
2195                             new HealthConnectException(errorCode, exception.toString())));
2196         } catch (RemoteException e) {
2197             Log.e(TAG, "Unable to send result to the callback", e);
2198         }
2199     }
2200 
tryAndThrowException( @onNull IActivityDatesResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2201     private static void tryAndThrowException(
2202             @NonNull IActivityDatesResponseCallback callback,
2203             @NonNull Exception exception,
2204             @HealthConnectException.ErrorCode int errorCode) {
2205         try {
2206             callback.onError(
2207                     new HealthConnectExceptionParcel(
2208                             new HealthConnectException(errorCode, exception.toString())));
2209         } catch (RemoteException e) {
2210             Log.e(TAG, "Unable to send result to the callback", e);
2211         }
2212     }
2213 
tryAndThrowException( @onNull IGetChangeLogTokenCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2214     private static void tryAndThrowException(
2215             @NonNull IGetChangeLogTokenCallback callback,
2216             @NonNull Exception exception,
2217             @HealthConnectException.ErrorCode int errorCode) {
2218         try {
2219             callback.onError(
2220                     new HealthConnectExceptionParcel(
2221                             new HealthConnectException(errorCode, exception.toString())));
2222         } catch (RemoteException e) {
2223             Log.e(TAG, "Unable to send result to the callback", e);
2224         }
2225     }
2226 
tryAndThrowException( @onNull IAccessLogsResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2227     private static void tryAndThrowException(
2228             @NonNull IAccessLogsResponseCallback callback,
2229             @NonNull Exception exception,
2230             @HealthConnectException.ErrorCode int errorCode) {
2231         try {
2232             callback.onError(
2233                     new HealthConnectExceptionParcel(
2234                             new HealthConnectException(errorCode, exception.toString())));
2235         } catch (RemoteException e) {
2236             Log.e(TAG, "Unable to send result to the callback", e);
2237         }
2238     }
2239 
tryAndThrowException( @onNull IEmptyResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2240     private static void tryAndThrowException(
2241             @NonNull IEmptyResponseCallback callback,
2242             @NonNull Exception exception,
2243             @HealthConnectException.ErrorCode int errorCode) {
2244         try {
2245             callback.onError(
2246                     new HealthConnectExceptionParcel(
2247                             new HealthConnectException(errorCode, exception.toString())));
2248         } catch (RemoteException e) {
2249             Log.e(TAG, "Unable to send result to the callback", e);
2250         }
2251     }
2252 
tryAndThrowException( @onNull IApplicationInfoResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2253     private static void tryAndThrowException(
2254             @NonNull IApplicationInfoResponseCallback callback,
2255             @NonNull Exception exception,
2256             @HealthConnectException.ErrorCode int errorCode) {
2257         try {
2258             callback.onError(
2259                     new HealthConnectExceptionParcel(
2260                             new HealthConnectException(errorCode, exception.toString())));
2261         } catch (RemoteException e) {
2262             Log.e(TAG, "Unable to send result to the callback", e);
2263         }
2264     }
2265 
tryAndThrowException( @onNull IChangeLogsResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2266     private static void tryAndThrowException(
2267             @NonNull IChangeLogsResponseCallback callback,
2268             @NonNull Exception exception,
2269             @HealthConnectException.ErrorCode int errorCode) {
2270         try {
2271             callback.onError(
2272                     new HealthConnectExceptionParcel(
2273                             new HealthConnectException(errorCode, exception.toString())));
2274         } catch (RemoteException e) {
2275             Log.e(TAG, "Unable to send result to the callback", e);
2276         }
2277     }
2278 
tryAndThrowException( @onNull IRecordTypeInfoResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2279     private static void tryAndThrowException(
2280             @NonNull IRecordTypeInfoResponseCallback callback,
2281             @NonNull Exception exception,
2282             @HealthConnectException.ErrorCode int errorCode) {
2283         try {
2284             callback.onError(
2285                     new HealthConnectExceptionParcel(
2286                             new HealthConnectException(errorCode, exception.toString())));
2287         } catch (RemoteException e) {
2288             Log.e(TAG, "Unable to send result to the callback", e);
2289         }
2290     }
2291 
tryAndThrowException( @onNull IGetPriorityResponseCallback callback, @NonNull Exception exception, @HealthConnectException.ErrorCode int errorCode)2292     private static void tryAndThrowException(
2293             @NonNull IGetPriorityResponseCallback callback,
2294             @NonNull Exception exception,
2295             @HealthConnectException.ErrorCode int errorCode) {
2296         try {
2297             callback.onError(
2298                     new HealthConnectExceptionParcel(
2299                             new HealthConnectException(errorCode, exception.toString())));
2300         } catch (RemoteException e) {
2301             Log.e(TAG, "Unable to send result to the callback", e);
2302         }
2303     }
2304 
tryAndThrowException( @onNull IMigrationCallback callback, @NonNull Exception exception, @MigrationException.ErrorCode int errorCode, @Nullable String failedEntityId)2305     private static void tryAndThrowException(
2306             @NonNull IMigrationCallback callback,
2307             @NonNull Exception exception,
2308             @MigrationException.ErrorCode int errorCode,
2309             @Nullable String failedEntityId) {
2310         try {
2311             callback.onError(
2312                     new MigrationException(exception.toString(), errorCode, failedEntityId));
2313         } catch (RemoteException e) {
2314             Log.e(TAG, "Unable to send result to the callback", e);
2315         }
2316     }
2317 }
2318