• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.health.connect;
18 
19 import static android.Manifest.permission.BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS;
20 import static android.Manifest.permission.RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS;
21 import static android.health.connect.Constants.DEFAULT_LONG;
22 import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE;
23 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
24 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
25 import static android.health.connect.HealthPermissions.WRITE_MEDICAL_DATA;
26 
27 import static com.android.healthfitness.flags.Flags.FLAG_CLOUD_BACKUP_AND_RESTORE;
28 import static com.android.healthfitness.flags.Flags.FLAG_IMMEDIATE_EXPORT;
29 import static com.android.healthfitness.flags.Flags.FLAG_PERSONAL_HEALTH_RECORD;
30 
31 import android.Manifest;
32 import android.annotation.CallbackExecutor;
33 import android.annotation.FlaggedApi;
34 import android.annotation.IntDef;
35 import android.annotation.IntRange;
36 import android.annotation.NonNull;
37 import android.annotation.Nullable;
38 import android.annotation.RequiresPermission;
39 import android.annotation.SdkConstant;
40 import android.annotation.SystemApi;
41 import android.annotation.SystemService;
42 import android.annotation.TestApi;
43 import android.annotation.UserHandleAware;
44 import android.annotation.WorkerThread;
45 import android.content.Context;
46 import android.content.pm.PackageInfo;
47 import android.content.pm.PackageManager;
48 import android.content.pm.PermissionGroupInfo;
49 import android.content.pm.PermissionInfo;
50 import android.health.connect.accesslog.AccessLog;
51 import android.health.connect.accesslog.AccessLogsResponseParcel;
52 import android.health.connect.aidl.ActivityDatesRequestParcel;
53 import android.health.connect.aidl.ActivityDatesResponseParcel;
54 import android.health.connect.aidl.AggregateDataRequestParcel;
55 import android.health.connect.aidl.AggregateDataResponseParcel;
56 import android.health.connect.aidl.ApplicationInfoResponseParcel;
57 import android.health.connect.aidl.DeleteUsingFiltersRequestParcel;
58 import android.health.connect.aidl.GetPriorityResponseParcel;
59 import android.health.connect.aidl.HealthConnectExceptionParcel;
60 import android.health.connect.aidl.IAccessLogsResponseCallback;
61 import android.health.connect.aidl.IActivityDatesResponseCallback;
62 import android.health.connect.aidl.IAggregateRecordsResponseCallback;
63 import android.health.connect.aidl.IApplicationInfoResponseCallback;
64 import android.health.connect.aidl.ICanRestoreResponseCallback;
65 import android.health.connect.aidl.IChangeLogsResponseCallback;
66 import android.health.connect.aidl.IDataStagingFinishedCallback;
67 import android.health.connect.aidl.IEmptyResponseCallback;
68 import android.health.connect.aidl.IGetChangeLogTokenCallback;
69 import android.health.connect.aidl.IGetChangesForBackupResponseCallback;
70 import android.health.connect.aidl.IGetHealthConnectDataStateCallback;
71 import android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback;
72 import android.health.connect.aidl.IGetLatestMetadataForBackupResponseCallback;
73 import android.health.connect.aidl.IGetPriorityResponseCallback;
74 import android.health.connect.aidl.IHealthConnectService;
75 import android.health.connect.aidl.IInsertRecordsResponseCallback;
76 import android.health.connect.aidl.IMedicalDataSourceResponseCallback;
77 import android.health.connect.aidl.IMedicalDataSourcesResponseCallback;
78 import android.health.connect.aidl.IMedicalResourceListParcelResponseCallback;
79 import android.health.connect.aidl.IMedicalResourceTypeInfosCallback;
80 import android.health.connect.aidl.IMedicalResourcesResponseCallback;
81 import android.health.connect.aidl.IMigrationCallback;
82 import android.health.connect.aidl.IReadMedicalResourcesResponseCallback;
83 import android.health.connect.aidl.IReadRecordsResponseCallback;
84 import android.health.connect.aidl.IRecordTypeInfoResponseCallback;
85 import android.health.connect.aidl.InsertRecordsResponseParcel;
86 import android.health.connect.aidl.MedicalResourceListParcel;
87 import android.health.connect.aidl.ReadRecordsResponseParcel;
88 import android.health.connect.aidl.RecordIdFiltersParcel;
89 import android.health.connect.aidl.RecordTypeInfoResponseParcel;
90 import android.health.connect.aidl.RecordsParcel;
91 import android.health.connect.aidl.UpdatePriorityRequestParcel;
92 import android.health.connect.aidl.UpsertMedicalResourceRequestsParcel;
93 import android.health.connect.backuprestore.BackupMetadata;
94 import android.health.connect.backuprestore.GetChangesForBackupResponse;
95 import android.health.connect.backuprestore.GetLatestMetadataForBackupResponse;
96 import android.health.connect.backuprestore.RestoreChange;
97 import android.health.connect.changelog.ChangeLogTokenRequest;
98 import android.health.connect.changelog.ChangeLogTokenResponse;
99 import android.health.connect.changelog.ChangeLogsRequest;
100 import android.health.connect.changelog.ChangeLogsResponse;
101 import android.health.connect.datatypes.AggregationType;
102 import android.health.connect.datatypes.DataOrigin;
103 import android.health.connect.datatypes.FhirResource;
104 import android.health.connect.datatypes.FhirVersion;
105 import android.health.connect.datatypes.MedicalDataSource;
106 import android.health.connect.datatypes.MedicalResource;
107 import android.health.connect.datatypes.Record;
108 import android.health.connect.exportimport.ExportImportDocumentProvider;
109 import android.health.connect.exportimport.IImportStatusCallback;
110 import android.health.connect.exportimport.IQueryDocumentProvidersCallback;
111 import android.health.connect.exportimport.IScheduledExportStatusCallback;
112 import android.health.connect.exportimport.ImportStatus;
113 import android.health.connect.exportimport.ScheduledExportSettings;
114 import android.health.connect.exportimport.ScheduledExportStatus;
115 import android.health.connect.internal.datatypes.RecordInternal;
116 import android.health.connect.internal.datatypes.utils.InternalExternalRecordConverter;
117 import android.health.connect.migration.HealthConnectMigrationUiState;
118 import android.health.connect.migration.MigrationEntity;
119 import android.health.connect.migration.MigrationEntityParcel;
120 import android.health.connect.migration.MigrationException;
121 import android.health.connect.restore.StageRemoteDataException;
122 import android.health.connect.restore.StageRemoteDataRequest;
123 import android.net.Uri;
124 import android.os.Binder;
125 import android.os.OutcomeReceiver;
126 import android.os.ParcelFileDescriptor;
127 import android.os.RemoteException;
128 import android.os.UserHandle;
129 import android.util.Log;
130 
131 import com.android.healthfitness.flags.Flags;
132 import com.android.internal.annotations.VisibleForTesting;
133 
134 import java.lang.annotation.Retention;
135 import java.lang.annotation.RetentionPolicy;
136 import java.time.Duration;
137 import java.time.Instant;
138 import java.time.LocalDate;
139 import java.time.Period;
140 import java.time.ZoneOffset;
141 import java.util.ArrayList;
142 import java.util.Collections;
143 import java.util.HashSet;
144 import java.util.List;
145 import java.util.Map;
146 import java.util.Objects;
147 import java.util.Set;
148 import java.util.concurrent.Executor;
149 import java.util.stream.Collectors;
150 
151 /**
152  * This class provides APIs to interact with the centralized HealthConnect storage maintained by the
153  * system.
154  *
155  * <p>HealthConnect is an offline, on-device storage that unifies data from multiple devices and
156  * apps into an ecosystem featuring.
157  *
158  * <ul>
159  *   <li>APIs to insert data of various types into the system.
160  * </ul>
161  *
162  * <p>The basic unit of data in HealthConnect is represented as a {@link Record} object, which is
163  * the base class for all the other data types such as {@link
164  * android.health.connect.datatypes.StepsRecord}.
165  */
166 @SystemService(Context.HEALTHCONNECT_SERVICE)
167 public class HealthConnectManager {
168     /**
169      * Used in conjunction with {@link android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} to
170      * launch UI to show an app’s health permission rationale/data policy.
171      *
172      * <p><b>Note:</b> Used by apps to define an intent filter in conjunction with {@link
173      * android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} that the HC UI can link out to.
174      */
175     // We use intent.category prefix to be compatible with HealthPermissions strings definitions.
176     @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
177     public static final String CATEGORY_HEALTH_PERMISSIONS =
178             "android.intent.category.HEALTH_PERMISSIONS";
179 
180     /**
181      * Activity action: Launch UI to manage (e.g. grant/revoke) health permissions.
182      *
183      * <p>Shows a list of apps which request at least one permission of the Health permission group.
184      *
185      * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the
186      * app requesting the action. Optional: Adding package name extras launches a UI to manager
187      * (e.g. grant/revoke) for this app.
188      */
189     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
190     public static final String ACTION_MANAGE_HEALTH_PERMISSIONS =
191             "android.health.connect.action.MANAGE_HEALTH_PERMISSIONS";
192 
193     /**
194      * Activity action: Launch UI to share the route associated with an exercise session.
195      *
196      * <p>Input: caller must provide `String` extra EXTRA_SESSION_ID
197      *
198      * <p>Result will be delivered via [Activity.onActivityResult] with `ExerciseRoute`
199      * EXTRA_EXERCISE_ROUTE.
200      */
201     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
202     public static final String ACTION_REQUEST_EXERCISE_ROUTE =
203             "android.health.connect.action.REQUEST_EXERCISE_ROUTE";
204 
205     /**
206      * A string ID of a session to be used with {@link #ACTION_REQUEST_EXERCISE_ROUTE}.
207      *
208      * <p>This is used to specify route of which exercise session we want to request.
209      */
210     public static final String EXTRA_SESSION_ID = "android.health.connect.extra.SESSION_ID";
211 
212     /**
213      * An exercise route requested via {@link #ACTION_REQUEST_EXERCISE_ROUTE}.
214      *
215      * <p>This is returned for a successful request to access a route associated with an exercise
216      * session.
217      */
218     public static final String EXTRA_EXERCISE_ROUTE = "android.health.connect.extra.EXERCISE_ROUTE";
219 
220     /**
221      * Activity action: Launch UI to show and manage (e.g. grant/revoke) health permissions.
222      *
223      * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the
224      * app requesting the action must be present. An app can open only its own page.
225      *
226      * <p>Input: caller must provide `String[]` extra [EXTRA_PERMISSIONS]
227      *
228      * <p>Result will be delivered via [Activity.onActivityResult] with `String[]`
229      * [EXTRA_PERMISSIONS] and `int[]` [EXTRA_PERMISSION_GRANT_RESULTS], similar to
230      * [Activity.onRequestPermissionsResult]
231      *
232      * @hide
233      */
234     @SystemApi
235     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
236     public static final String ACTION_REQUEST_HEALTH_PERMISSIONS =
237             "android.health.connect.action.REQUEST_HEALTH_PERMISSIONS";
238 
239     /**
240      * Activity action: Launch UI to health connect home settings screen.
241      *
242      * <p>shows a list of recent apps that accessed (e.g. read/write) health data and allows the
243      * user to access health permissions and health data.
244      *
245      * @hide
246      */
247     @SystemApi
248     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
249     public static final String ACTION_HEALTH_HOME_SETTINGS =
250             "android.health.connect.action.HEALTH_HOME_SETTINGS";
251 
252     /**
253      * Activity action: Launch UI to show and manage (e.g. delete/export) health data.
254      *
255      * <p>shows a list of health data categories and actions to manage (e.g. delete/export) health
256      * data.
257      *
258      * @hide
259      */
260     @SystemApi
261     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
262     public static final String ACTION_MANAGE_HEALTH_DATA =
263             "android.health.connect.action.MANAGE_HEALTH_DATA";
264 
265     /**
266      * Activity action: Display information regarding migration - e.g. asking the user to take some
267      * action (e.g. update the system) so that migration can take place.
268      *
269      * <p><b>Note:</b> Callers of the migration APIs must handle this intent.
270      *
271      * @hide
272      */
273     @SystemApi
274     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
275     public static final String ACTION_SHOW_MIGRATION_INFO =
276             "android.health.connect.action.SHOW_MIGRATION_INFO";
277 
278     /**
279      * Broadcast Action: Health Connect is ready to accept migrated data.
280      *
281      * <p class="note">This broadcast is explicitly sent to Health Connect migration aware
282      * applications to prompt them to start/continue HC data migration. Migration aware applications
283      * are those that both hold {@code android.permission.MIGRATE_HEALTH_CONNECT_DATA} and handle
284      * {@code android.health.connect.action.SHOW_MIGRATION_INFO}.
285      *
286      * <p class="note">This is a protected intent that can only be sent by the system.
287      *
288      * @hide
289      */
290     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
291     @SystemApi
292     public static final String ACTION_HEALTH_CONNECT_MIGRATION_READY =
293             "android.health.connect.action.HEALTH_CONNECT_MIGRATION_READY";
294 
295     /**
296      * Unknown download state considered to be the default download state.
297      *
298      * <p>See also {@link #updateDataDownloadState}
299      *
300      * @hide
301      */
302     @SystemApi public static final int DATA_DOWNLOAD_STATE_UNKNOWN = 0;
303 
304     /**
305      * Indicates that the download has started.
306      *
307      * <p>See also {@link #updateDataDownloadState}
308      *
309      * @hide
310      */
311     @SystemApi public static final int DATA_DOWNLOAD_STARTED = 1;
312 
313     /**
314      * Indicates that the download is being retried.
315      *
316      * <p>See also {@link #updateDataDownloadState}
317      *
318      * @hide
319      */
320     @SystemApi public static final int DATA_DOWNLOAD_RETRY = 2;
321 
322     /**
323      * Indicates that the download has failed.
324      *
325      * <p>See also {@link #updateDataDownloadState}
326      *
327      * @hide
328      */
329     @SystemApi public static final int DATA_DOWNLOAD_FAILED = 3;
330 
331     /**
332      * Indicates that the download has completed.
333      *
334      * <p>See also {@link HealthConnectManager#updateDataDownloadState}
335      *
336      * @hide
337      */
338     @SystemApi public static final int DATA_DOWNLOAD_COMPLETE = 4;
339 
340     /**
341      * Activity action: Launch activity exported by client application that handles onboarding to
342      * Health Connect.
343      *
344      * <p>Health Connect will invoke this intent whenever the user attempts to connect an app that
345      * has exported an activity that responds to this intent. The launched activity is responsible
346      * for making permission requests and any other prerequisites for connecting to Health Connect.
347      *
348      * <p class="note">Applications exporting an activity that is launched by this intent must also
349      * guard it with {@link HealthPermissions#START_ONBOARDING} so that only the system can launch
350      * it.
351      *
352      * @hide
353      */
354     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
355     public static final String ACTION_SHOW_ONBOARDING =
356             "android.health.connect.action.SHOW_ONBOARDING";
357 
358     private static final String TAG = "HealthConnectManager";
359     private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health.";
360 
361     @Nullable private static volatile Set<String> sHealthPermissions;
362 
363     private final Context mContext;
364     private final IHealthConnectService mService;
365     private final InternalExternalRecordConverter mInternalExternalRecordConverter;
366 
367     /** @hide */
HealthConnectManager(@onNull Context context, @NonNull IHealthConnectService service)368     HealthConnectManager(@NonNull Context context, @NonNull IHealthConnectService service) {
369         mContext = context;
370         mService = service;
371         mInternalExternalRecordConverter = InternalExternalRecordConverter.getInstance();
372     }
373 
374     /**
375      * Grant a runtime permission to an application which the application does not already have. The
376      * permission must have been requested by the application. If the application is not allowed to
377      * hold the permission, a {@link java.lang.SecurityException} is thrown. If the package or
378      * permission is invalid, a {@link java.lang.IllegalArgumentException} is thrown.
379      *
380      * <p><b>Note:</b> This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET}.
381      *
382      * @hide
383      */
384     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
385     @UserHandleAware
grantHealthPermission(@onNull String packageName, @NonNull String permissionName)386     public void grantHealthPermission(@NonNull String packageName, @NonNull String permissionName) {
387         try {
388             mService.grantHealthPermission(packageName, permissionName, mContext.getUser());
389         } catch (RemoteException e) {
390             throw e.rethrowFromSystemServer();
391         }
392     }
393 
394     /**
395      * Revoke a health permission that was previously granted by {@link
396      * #grantHealthPermission(String, String)} The permission must have been requested by the
397      * application. If the application is not allowed to hold the permission, a {@link
398      * java.lang.SecurityException} is thrown. If the package or permission is invalid, a {@link
399      * java.lang.IllegalArgumentException} is thrown.
400      *
401      * <p><b>Note:</b> This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET} or {@code
402      * PackageManager.FLAG_PERMISSION_USER_FIXED} based on the number of revocations of a particular
403      * permission for a package.
404      *
405      * @hide
406      */
407     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
408     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
409     @UserHandleAware
revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason)410     public void revokeHealthPermission(
411             @NonNull String packageName, @NonNull String permissionName, @Nullable String reason) {
412         try {
413             mService.revokeHealthPermission(
414                     packageName, permissionName, reason, mContext.getUser());
415         } catch (RemoteException e) {
416             throw e.rethrowFromSystemServer();
417         }
418     }
419 
420     /**
421      * Revokes all health permissions that were previously granted by {@link
422      * #grantHealthPermission(String, String)} If the package is invalid, a {@link
423      * java.lang.IllegalArgumentException} is thrown.
424      *
425      * @hide
426      */
427     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
428     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
429     @UserHandleAware
revokeAllHealthPermissions(@onNull String packageName, @Nullable String reason)430     public void revokeAllHealthPermissions(@NonNull String packageName, @Nullable String reason) {
431         try {
432             mService.revokeAllHealthPermissions(packageName, reason, mContext.getUser());
433         } catch (RemoteException e) {
434             throw e.rethrowFromSystemServer();
435         }
436     }
437 
438     /**
439      * Returns a list of health permissions that were previously granted by {@link
440      * #grantHealthPermission(String, String)}.
441      *
442      * @hide
443      */
444     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
445     @UserHandleAware
getGrantedHealthPermissions(@onNull String packageName)446     public List<String> getGrantedHealthPermissions(@NonNull String packageName) {
447         try {
448             return mService.getGrantedHealthPermissions(packageName, mContext.getUser());
449         } catch (RemoteException e) {
450             throw e.rethrowFromSystemServer();
451         }
452     }
453 
454     /**
455      * Returns permission flags for the given package name and Health permissions.
456      *
457      * <p>This is equivalent to calling {@link PackageManager#getPermissionFlags(String, String,
458      * UserHandle)} for each provided permission except it throws an exception for non-Health or
459      * undeclared permissions. Flag masks listed in {@link PackageManager#MASK_PERMISSION_FLAGS_ALL}
460      * can be used to check the flag values.
461      *
462      * <p>Returned flags for invalid, non-Health or undeclared permissions are equal to zero.
463      *
464      * @return a map which contains all requested permissions as keys and corresponding flags as
465      *     values.
466      * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not
467      *     Health permissions or not declared by the app.
468      * @throws NullPointerException if any of the arguments is {@code null}.
469      * @throws SecurityException if the caller doesn't possess {@code
470      *     android.permission.MANAGE_HEALTH_PERMISSIONS}.
471      * @hide
472      */
473     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
474     @UserHandleAware
getHealthPermissionsFlags( @onNull String packageName, @NonNull List<String> permissions)475     public Map<String, Integer> getHealthPermissionsFlags(
476             @NonNull String packageName, @NonNull List<String> permissions) {
477         try {
478             return mService.getHealthPermissionsFlags(packageName, mContext.getUser(), permissions);
479         } catch (RemoteException e) {
480             throw e.rethrowFromSystemServer();
481         }
482     }
483 
484     /**
485      * Sets/clears {@link PackageManager#FLAG_PERMISSION_USER_FIXED} for given health permissions.
486      *
487      * @param value whether to set or clear the flag, {@code true} means set, {@code false} - clear.
488      * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not
489      *     Health permissions or not declared by the app.
490      * @throws NullPointerException if any of the arguments is {@code null}.
491      * @throws SecurityException if the caller doesn't possess {@code
492      *     android.permission.MANAGE_HEALTH_PERMISSIONS}.
493      * @hide
494      */
495     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
496     @UserHandleAware
setHealthPermissionsUserFixedFlagValue( @onNull String packageName, @NonNull List<String> permissions, boolean value)497     public void setHealthPermissionsUserFixedFlagValue(
498             @NonNull String packageName, @NonNull List<String> permissions, boolean value) {
499         try {
500             mService.setHealthPermissionsUserFixedFlagValue(
501                     packageName, mContext.getUser(), permissions, value);
502         } catch (RemoteException e) {
503             throw e.rethrowFromSystemServer();
504         }
505     }
506 
507     /**
508      * Returns the date from which an app have access to the historical health data. Returns null if
509      * the package doesn't have historical access date.
510      *
511      * @hide
512      */
513     @RequiresPermission(HealthPermissions.MANAGE_HEALTH_PERMISSIONS)
514     @UserHandleAware
515     @Nullable
getHealthDataHistoricalAccessStartDate(@onNull String packageName)516     public Instant getHealthDataHistoricalAccessStartDate(@NonNull String packageName) {
517         try {
518             long dateMilli =
519                     mService.getHistoricalAccessStartDateInMilliseconds(
520                             packageName, mContext.getUser());
521             if (dateMilli == DEFAULT_LONG) {
522                 return null;
523             } else {
524                 return Instant.ofEpochMilli(dateMilli);
525             }
526         } catch (RemoteException e) {
527             throw e.rethrowFromSystemServer();
528         }
529     }
530 
531     /**
532      * Inserts {@code records} into the HealthConnect database. The records returned in {@link
533      * InsertRecordsResponse} contains the unique IDs of the input records. The values are in same
534      * order as {@code records}. In case of an error or a permission failure the HealthConnect
535      * service, {@link OutcomeReceiver#onError} will be invoked with a {@link
536      * HealthConnectException}.
537      *
538      * @param records list of records to be inserted.
539      * @param executor Executor on which to invoke the callback.
540      * @param callback Callback to receive result of performing this operation.
541      * @throws RuntimeException for internal errors
542      */
insertRecords( @onNull List<Record> records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback)543     public void insertRecords(
544             @NonNull List<Record> records,
545             @NonNull @CallbackExecutor Executor executor,
546             @NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback) {
547         Objects.requireNonNull(records);
548         Objects.requireNonNull(executor);
549         Objects.requireNonNull(callback);
550         try {
551             // Unset any set ids for insert. This is to prevent random string ids from creating
552             // illegal argument exception.
553             records.forEach((record) -> record.getMetadata().setId(""));
554             List<RecordInternal<?>> recordInternals =
555                     records.stream()
556                             .map(
557                                     record ->
558                                             record.toRecordInternal()
559                                                     .setPackageName(mContext.getPackageName()))
560                             .collect(Collectors.toList());
561             mService.insertRecords(
562                     mContext.getAttributionSource(),
563                     new RecordsParcel(recordInternals),
564                     new IInsertRecordsResponseCallback.Stub() {
565                         @Override
566                         public void onResult(InsertRecordsResponseParcel parcel) {
567                             Binder.clearCallingIdentity();
568                             executor.execute(
569                                     () ->
570                                             callback.onResult(
571                                                     new InsertRecordsResponse(
572                                                             toExternalRecordsWithUuids(
573                                                                     recordInternals,
574                                                                     parcel.getUids()))));
575                         }
576 
577                         @Override
578                         public void onError(HealthConnectExceptionParcel exception) {
579                             returnError(executor, exception, callback);
580                         }
581                     });
582         } catch (RemoteException e) {
583             throw e.rethrowFromSystemServer();
584         }
585     }
586 
587     /**
588      * Get aggregations corresponding to {@code request}.
589      *
590      * @param <T> Result type of the aggregation.
591      *     <p>Note:
592      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
593      *     typed in nature.
594      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
595      * @param request request for different aggregation.
596      * @param executor Executor on which to invoke the callback.
597      * @param callback Callback to receive result of performing this operation.
598      * @see AggregateRecordsResponse#get
599      */
600     @SuppressWarnings("unchecked")
aggregate( @onNull AggregateRecordsRequest<T> request, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<AggregateRecordsResponse<T>, HealthConnectException> callback)601     public <T> void aggregate(
602             @NonNull AggregateRecordsRequest<T> request,
603             @NonNull @CallbackExecutor Executor executor,
604             @NonNull
605                     OutcomeReceiver<AggregateRecordsResponse<T>, HealthConnectException> callback) {
606         Objects.requireNonNull(request);
607         Objects.requireNonNull(executor);
608         Objects.requireNonNull(callback);
609         try {
610             mService.aggregateRecords(
611                     mContext.getAttributionSource(),
612                     new AggregateDataRequestParcel(request),
613                     new IAggregateRecordsResponseCallback.Stub() {
614                         @Override
615                         public void onResult(AggregateDataResponseParcel parcel) {
616                             Binder.clearCallingIdentity();
617                             try {
618                                 executor.execute(
619                                         () ->
620                                                 callback.onResult(
621                                                         (AggregateRecordsResponse<T>)
622                                                                 parcel.getAggregateDataResponse()));
623                             } catch (Exception exception) {
624                                 callback.onError(
625                                         new HealthConnectException(
626                                                 HealthConnectException.ERROR_INTERNAL));
627                             }
628                         }
629 
630                         @Override
631                         public void onError(HealthConnectExceptionParcel exception) {
632                             returnError(executor, exception, callback);
633                         }
634                     });
635         } catch (ClassCastException classCastException) {
636             returnError(
637                     executor,
638                     new HealthConnectExceptionParcel(
639                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
640                     callback);
641         } catch (RemoteException e) {
642             throw e.rethrowFromSystemServer();
643         }
644     }
645 
646     /**
647      * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped
648      * by concrete intervals of time, for example 5 Hrs, 10 Hrs etc.
649      *
650      * @param <T> Result type of the aggregation.
651      *     <p>Note:
652      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
653      *     typed in nature.
654      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
655      * @param request request for different aggregation.
656      * @param duration Duration on which to group by results
657      * @param executor Executor on which to invoke the callback.
658      * @param callback Callback to receive result of performing this operation.
659      * @see HealthConnectManager#aggregateGroupByPeriod
660      */
661     @SuppressWarnings("unchecked")
aggregateGroupByDuration( @onNull AggregateRecordsRequest<T> request, @NonNull Duration duration, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List<AggregateRecordsGroupedByDurationResponse<T>>, HealthConnectException> callback)662     public <T> void aggregateGroupByDuration(
663             @NonNull AggregateRecordsRequest<T> request,
664             @NonNull Duration duration,
665             @NonNull @CallbackExecutor Executor executor,
666             @NonNull
667                     OutcomeReceiver<
668                                     List<AggregateRecordsGroupedByDurationResponse<T>>,
669                                     HealthConnectException>
670                             callback) {
671         Objects.requireNonNull(request);
672         Objects.requireNonNull(duration);
673         if (duration.toMillis() < 1) {
674             throw new IllegalArgumentException("Duration should be at least 1 millisecond");
675         }
676         Objects.requireNonNull(executor);
677         Objects.requireNonNull(callback);
678         try {
679             mService.aggregateRecords(
680                     mContext.getAttributionSource(),
681                     new AggregateDataRequestParcel(request, duration),
682                     new IAggregateRecordsResponseCallback.Stub() {
683                         @Override
684                         public void onResult(AggregateDataResponseParcel parcel) {
685                             Binder.clearCallingIdentity();
686                             List<AggregateRecordsGroupedByDurationResponse<T>> result =
687                                     new ArrayList<>();
688                             for (AggregateRecordsGroupedByDurationResponse<?>
689                                     aggregateRecordsGroupedByDurationResponse :
690                                             parcel.getAggregateDataResponseGroupedByDuration()) {
691                                 result.add(
692                                         (AggregateRecordsGroupedByDurationResponse<T>)
693                                                 aggregateRecordsGroupedByDurationResponse);
694                             }
695                             executor.execute(() -> callback.onResult(result));
696                         }
697 
698                         @Override
699                         public void onError(HealthConnectExceptionParcel exception) {
700                             returnError(executor, exception, callback);
701                         }
702                     });
703         } catch (ClassCastException classCastException) {
704             returnError(
705                     executor,
706                     new HealthConnectExceptionParcel(
707                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
708                     callback);
709         } catch (RemoteException e) {
710             throw e.rethrowFromSystemServer();
711         }
712     }
713 
714     /**
715      * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped
716      * by number of days. This API handles changes in {@link ZoneOffset} when computing the data on
717      * a per-day basis.
718      *
719      * @param <T> Result type of the aggregation.
720      *     <p>Note:
721      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
722      *     typed in nature.
723      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
724      * @param request Request for different aggregation.
725      * @param period Period on which to group by results
726      * @param executor Executor on which to invoke the callback.
727      * @param callback Callback to receive result of performing this operation.
728      * @see AggregateRecordsGroupedByPeriodResponse#get
729      * @see HealthConnectManager#aggregateGroupByDuration
730      */
731     @SuppressWarnings("unchecked")
aggregateGroupByPeriod( @onNull AggregateRecordsRequest<T> request, @NonNull Period period, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List<AggregateRecordsGroupedByPeriodResponse<T>>, HealthConnectException> callback)732     public <T> void aggregateGroupByPeriod(
733             @NonNull AggregateRecordsRequest<T> request,
734             @NonNull Period period,
735             @NonNull @CallbackExecutor Executor executor,
736             @NonNull
737                     OutcomeReceiver<
738                                     List<AggregateRecordsGroupedByPeriodResponse<T>>,
739                                     HealthConnectException>
740                             callback) {
741         Objects.requireNonNull(request);
742         Objects.requireNonNull(period);
743         if (period == Period.ZERO) {
744             throw new IllegalArgumentException("Period duration should be at least a day");
745         }
746         Objects.requireNonNull(executor);
747         Objects.requireNonNull(callback);
748         try {
749             mService.aggregateRecords(
750                     mContext.getAttributionSource(),
751                     new AggregateDataRequestParcel(request, period),
752                     new IAggregateRecordsResponseCallback.Stub() {
753                         @Override
754                         public void onResult(AggregateDataResponseParcel parcel) {
755                             Binder.clearCallingIdentity();
756                             List<AggregateRecordsGroupedByPeriodResponse<T>> result =
757                                     new ArrayList<>();
758                             for (AggregateRecordsGroupedByPeriodResponse<?>
759                                     aggregateRecordsGroupedByPeriodResponse :
760                                             parcel.getAggregateDataResponseGroupedByPeriod()) {
761                                 result.add(
762                                         (AggregateRecordsGroupedByPeriodResponse<T>)
763                                                 aggregateRecordsGroupedByPeriodResponse);
764                             }
765 
766                             executor.execute(() -> callback.onResult(result));
767                         }
768 
769                         @Override
770                         public void onError(HealthConnectExceptionParcel exception) {
771                             returnError(executor, exception, callback);
772                         }
773                     });
774         } catch (ClassCastException classCastException) {
775             returnError(
776                     executor,
777                     new HealthConnectExceptionParcel(
778                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
779                     callback);
780         } catch (RemoteException e) {
781             throw e.rethrowFromSystemServer();
782         }
783     }
784 
785     /**
786      * Deletes records based on the {@link DeleteUsingFiltersRequest}. This is only to be used by
787      * health connect controller APK(s). Ids that don't exist will be ignored.
788      *
789      * @param request Request based on which to perform delete operation
790      * @param executor Executor on which to invoke the callback.
791      * @param callback Callback to receive result of performing this operation.
792      * @hide
793      */
794     @SystemApi
795     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
deleteRecords( @onNull DeleteUsingFiltersRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)796     public void deleteRecords(
797             @NonNull DeleteUsingFiltersRequest request,
798             @NonNull Executor executor,
799             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
800         Objects.requireNonNull(request);
801         Objects.requireNonNull(executor);
802         Objects.requireNonNull(callback);
803 
804         try {
805             mService.deleteUsingFilters(
806                     mContext.getAttributionSource(),
807                     new DeleteUsingFiltersRequestParcel(request),
808                     new IEmptyResponseCallback.Stub() {
809                         @Override
810                         public void onResult() {
811                             executor.execute(() -> callback.onResult(null));
812                         }
813 
814                         @Override
815                         public void onError(HealthConnectExceptionParcel exception) {
816                             returnError(executor, exception, callback);
817                         }
818                     });
819         } catch (RemoteException remoteException) {
820             remoteException.rethrowFromSystemServer();
821         }
822     }
823 
824     /**
825      * Deletes records based on {@link RecordIdFilter}.
826      *
827      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
828      *
829      * @param recordIds recordIds on which to perform delete operation.
830      * @param executor Executor on which to invoke the callback.
831      * @param callback Callback to receive result of performing this operation.
832      * @throws IllegalArgumentException if {@code recordIds is empty}
833      */
deleteRecords( @onNull List<RecordIdFilter> recordIds, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)834     public void deleteRecords(
835             @NonNull List<RecordIdFilter> recordIds,
836             @NonNull Executor executor,
837             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
838         Objects.requireNonNull(recordIds);
839         Objects.requireNonNull(executor);
840         Objects.requireNonNull(callback);
841 
842         if (recordIds.isEmpty()) {
843             throw new IllegalArgumentException("record ids can't be empty");
844         }
845 
846         try {
847             mService.deleteUsingFilters(
848                     mContext.getAttributionSource(),
849                     new DeleteUsingFiltersRequestParcel(
850                             new RecordIdFiltersParcel(recordIds), mContext.getPackageName()),
851                     new IEmptyResponseCallback.Stub() {
852                         @Override
853                         public void onResult() {
854                             executor.execute(() -> callback.onResult(null));
855                         }
856 
857                         @Override
858                         public void onError(HealthConnectExceptionParcel exception) {
859                             returnError(executor, exception, callback);
860                         }
861                     });
862         } catch (RemoteException remoteException) {
863             remoteException.rethrowFromSystemServer();
864         }
865     }
866 
867     /**
868      * Deletes records based on the {@link TimeRangeFilter}.
869      *
870      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
871      *
872      * @param recordType recordType to perform delete operation on.
873      * @param timeRangeFilter time filter based on which to delete the records.
874      * @param executor Executor on which to invoke the callback.
875      * @param callback Callback to receive result of performing this operation.
876      */
deleteRecords( @onNull Class<? extends Record> recordType, @NonNull TimeRangeFilter timeRangeFilter, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)877     public void deleteRecords(
878             @NonNull Class<? extends Record> recordType,
879             @NonNull TimeRangeFilter timeRangeFilter,
880             @NonNull Executor executor,
881             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
882         Objects.requireNonNull(recordType);
883         Objects.requireNonNull(timeRangeFilter);
884         Objects.requireNonNull(executor);
885         Objects.requireNonNull(callback);
886 
887         try {
888             mService.deleteUsingFilters(
889                     mContext.getAttributionSource(),
890                     new DeleteUsingFiltersRequestParcel(
891                             new DeleteUsingFiltersRequest.Builder()
892                                     .addDataOrigin(
893                                             new DataOrigin.Builder()
894                                                     .setPackageName(mContext.getPackageName())
895                                                     .build())
896                                     .addRecordType(recordType)
897                                     .setTimeRangeFilter(timeRangeFilter)
898                                     .build()),
899                     new IEmptyResponseCallback.Stub() {
900                         @Override
901                         public void onResult() {
902                             executor.execute(() -> callback.onResult(null));
903                         }
904 
905                         @Override
906                         public void onError(HealthConnectExceptionParcel exception) {
907                             returnError(executor, exception, callback);
908                         }
909                     });
910         } catch (RemoteException remoteException) {
911             remoteException.rethrowFromSystemServer();
912         }
913     }
914 
915     /**
916      * Get change logs post the time when {@code token} was generated.
917      *
918      * @param changeLogsRequest The token from {@link HealthConnectManager#getChangeLogToken}.
919      * @param executor Executor on which to invoke the callback.
920      * @param callback Callback to receive result of performing this operation.
921      * @see HealthConnectManager#getChangeLogToken
922      */
getChangeLogs( @onNull ChangeLogsRequest changeLogsRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback)923     public void getChangeLogs(
924             @NonNull ChangeLogsRequest changeLogsRequest,
925             @NonNull @CallbackExecutor Executor executor,
926             @NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback) {
927         Objects.requireNonNull(changeLogsRequest);
928         Objects.requireNonNull(executor);
929         Objects.requireNonNull(callback);
930 
931         try {
932             mService.getChangeLogs(
933                     mContext.getAttributionSource(),
934                     changeLogsRequest,
935                     new IChangeLogsResponseCallback.Stub() {
936                         @Override
937                         public void onResult(ChangeLogsResponse parcel) {
938                             Binder.clearCallingIdentity();
939                             executor.execute(() -> callback.onResult(parcel));
940                         }
941 
942                         @Override
943                         public void onError(HealthConnectExceptionParcel exception) {
944                             returnError(executor, exception, callback);
945                         }
946                     });
947         } catch (ClassCastException invalidArgumentException) {
948             callback.onError(
949                     new HealthConnectException(
950                             HealthConnectException.ERROR_INVALID_ARGUMENT,
951                             invalidArgumentException.getMessage()));
952         } catch (RemoteException e) {
953             throw e.rethrowFromSystemServer();
954         }
955     }
956 
957     /**
958      * Get token for {HealthConnectManager#getChangeLogs}. Changelogs requested corresponding to
959      * this token will be post the time this token was generated by the system all items that match
960      * the given filters.
961      *
962      * <p>Tokens from this request are to be passed to {HealthConnectManager#getChangeLogs}
963      *
964      * @param request A request to get changelog token
965      * @param executor Executor on which to invoke the callback.
966      * @param callback Callback to receive result of performing this operation.
967      */
getChangeLogToken( @onNull ChangeLogTokenRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<ChangeLogTokenResponse, HealthConnectException> callback)968     public void getChangeLogToken(
969             @NonNull ChangeLogTokenRequest request,
970             @NonNull Executor executor,
971             @NonNull OutcomeReceiver<ChangeLogTokenResponse, HealthConnectException> callback) {
972         try {
973             mService.getChangeLogToken(
974                     mContext.getAttributionSource(),
975                     request,
976                     new IGetChangeLogTokenCallback.Stub() {
977                         @Override
978                         public void onResult(ChangeLogTokenResponse parcel) {
979                             Binder.clearCallingIdentity();
980                             executor.execute(() -> callback.onResult(parcel));
981                         }
982 
983                         @Override
984                         public void onError(HealthConnectExceptionParcel exception) {
985                             returnError(executor, exception, callback);
986                         }
987                     });
988         } catch (RemoteException e) {
989             throw e.rethrowFromSystemServer();
990         }
991     }
992 
993     /**
994      * Fetch the data priority order of the contributing {@link DataOrigin} for {@code
995      * dataCategory}.
996      *
997      * @param dataCategory {@link HealthDataCategory} for which to get the priority order
998      * @param executor Executor on which to invoke the callback.
999      * @param callback Callback to receive result of performing this operation.
1000      * @hide
1001      */
1002     @SystemApi
1003     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
fetchDataOriginsPriorityOrder( @ealthDataCategory.Type int dataCategory, @NonNull Executor executor, @NonNull OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, HealthConnectException> callback)1004     public void fetchDataOriginsPriorityOrder(
1005             @HealthDataCategory.Type int dataCategory,
1006             @NonNull Executor executor,
1007             @NonNull
1008                     OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, HealthConnectException>
1009                             callback) {
1010         try {
1011             mService.getCurrentPriority(
1012                     dataCategory,
1013                     new IGetPriorityResponseCallback.Stub() {
1014                         @Override
1015                         public void onResult(GetPriorityResponseParcel response) {
1016                             Binder.clearCallingIdentity();
1017                             executor.execute(
1018                                     () -> callback.onResult(response.getPriorityResponse()));
1019                         }
1020 
1021                         @Override
1022                         public void onError(HealthConnectExceptionParcel exception) {
1023                             returnError(executor, exception, callback);
1024                         }
1025                     });
1026         } catch (RemoteException e) {
1027             throw e.rethrowFromSystemServer();
1028         }
1029     }
1030 
1031     /**
1032      * Updates the priority order of the apps as per {@code request}
1033      *
1034      * @param request new priority order update request
1035      * @param executor Executor on which to invoke the callback.
1036      * @param callback Callback to receive result of performing this operation.
1037      * @hide
1038      */
1039     @SystemApi
1040     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
updateDataOriginPriorityOrder( @onNull UpdateDataOriginPriorityOrderRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1041     public void updateDataOriginPriorityOrder(
1042             @NonNull UpdateDataOriginPriorityOrderRequest request,
1043             @NonNull Executor executor,
1044             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1045         try {
1046             mService.updatePriority(
1047                     new UpdatePriorityRequestParcel(request),
1048                     new IEmptyResponseCallback.Stub() {
1049                         @Override
1050                         public void onResult() {
1051                             Binder.clearCallingIdentity();
1052                             executor.execute(() -> callback.onResult(null));
1053                         }
1054 
1055                         @Override
1056                         public void onError(HealthConnectExceptionParcel exception) {
1057                             returnError(executor, exception, callback);
1058                         }
1059                     });
1060         } catch (RemoteException e) {
1061             throw e.rethrowFromSystemServer();
1062         }
1063     }
1064 
1065     /**
1066      * Retrieves {@link RecordTypeInfoResponse} for each RecordType.
1067      *
1068      * @param executor Executor on which to invoke the callback.
1069      * @param callback Callback to receive result of performing this operation.
1070      * @hide
1071      */
1072     @SystemApi
1073     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryAllRecordTypesInfo( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver< Map<Class<? extends Record>, RecordTypeInfoResponse>, HealthConnectException> callback)1074     public void queryAllRecordTypesInfo(
1075             @NonNull @CallbackExecutor Executor executor,
1076             @NonNull
1077                     OutcomeReceiver<
1078                                     Map<Class<? extends Record>, RecordTypeInfoResponse>,
1079                                     HealthConnectException>
1080                             callback) {
1081         Objects.requireNonNull(executor);
1082         Objects.requireNonNull(callback);
1083         try {
1084             mService.queryAllRecordTypesInfo(
1085                     new IRecordTypeInfoResponseCallback.Stub() {
1086                         @Override
1087                         public void onResult(RecordTypeInfoResponseParcel parcel) {
1088                             Binder.clearCallingIdentity();
1089                             executor.execute(
1090                                     () -> callback.onResult(parcel.getRecordTypeInfoResponses()));
1091                         }
1092 
1093                         @Override
1094                         public void onError(HealthConnectExceptionParcel exception) {
1095                             returnError(executor, exception, callback);
1096                         }
1097                     });
1098         } catch (RemoteException e) {
1099             throw e.rethrowFromSystemServer();
1100         }
1101     }
1102 
1103     /**
1104      * Returns currently set auto delete period for this user.
1105      *
1106      * <p>If you are calling this function for the first time after a user unlock, this might take
1107      * some time so consider calling this on a thread.
1108      *
1109      * @return Auto delete period in days, 0 is returned if auto delete period is not set.
1110      * @throws RuntimeException for internal errors
1111      * @hide
1112      */
1113     @SystemApi
1114     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
1115     @IntRange(from = 0, to = 7300)
getRecordRetentionPeriodInDays()1116     public int getRecordRetentionPeriodInDays() {
1117         try {
1118             return mService.getRecordRetentionPeriodInDays(mContext.getUser());
1119         } catch (RemoteException e) {
1120             throw e.rethrowFromSystemServer();
1121         }
1122     }
1123 
1124     /**
1125      * Sets auto delete period (for all the records to be automatically deleted) for this user.
1126      *
1127      * <p>Note: The max value of auto delete period can be 7300 i.e. ~20 years
1128      *
1129      * @param days Auto period to be set in days. Use 0 to unset this value.
1130      * @param executor Executor on which to invoke the callback.
1131      * @param callback Callback to receive result of performing this operation.
1132      * @throws RuntimeException for internal errors
1133      * @throws IllegalArgumentException if {@code days} is not between 0 and 7300
1134      * @hide
1135      */
1136     @SystemApi
1137     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
setRecordRetentionPeriodInDays( @ntRangefrom = 0, to = 7300) int days, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1138     public void setRecordRetentionPeriodInDays(
1139             @IntRange(from = 0, to = 7300) int days,
1140             @NonNull Executor executor,
1141             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1142         Objects.requireNonNull(executor);
1143         Objects.requireNonNull(callback);
1144 
1145         if (days < 0 || days > 7300) {
1146             throw new IllegalArgumentException("days should be between " + 0 + " and " + 7300);
1147         }
1148 
1149         try {
1150             mService.setRecordRetentionPeriodInDays(
1151                     days,
1152                     mContext.getUser(),
1153                     new IEmptyResponseCallback.Stub() {
1154                         @Override
1155                         public void onResult() {
1156                             Binder.clearCallingIdentity();
1157                             executor.execute(() -> callback.onResult(null));
1158                         }
1159 
1160                         @Override
1161                         public void onError(HealthConnectExceptionParcel exception) {
1162                             returnError(executor, exception, callback);
1163                         }
1164                     });
1165         } catch (RemoteException e) {
1166             e.rethrowFromSystemServer();
1167         }
1168     }
1169 
1170     /**
1171      * Returns a list of access logs with package name and its access time for each record type.
1172      *
1173      * @param executor Executor on which to invoke the callback.
1174      * @param callback Callback to receive result of performing this operation.
1175      * @hide
1176      */
1177     @SystemApi
1178     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryAccessLogs( @onNull Executor executor, @NonNull OutcomeReceiver<List<AccessLog>, HealthConnectException> callback)1179     public void queryAccessLogs(
1180             @NonNull Executor executor,
1181             @NonNull OutcomeReceiver<List<AccessLog>, HealthConnectException> callback) {
1182         Objects.requireNonNull(executor);
1183         Objects.requireNonNull(callback);
1184         try {
1185             mService.queryAccessLogs(
1186                     mContext.getPackageName(),
1187                     new IAccessLogsResponseCallback.Stub() {
1188                         @Override
1189                         public void onResult(AccessLogsResponseParcel parcel) {
1190                             Binder.clearCallingIdentity();
1191                             executor.execute(() -> callback.onResult(parcel.getAccessLogs()));
1192                         }
1193 
1194                         @Override
1195                         public void onError(HealthConnectExceptionParcel exception) {
1196                             returnError(executor, exception, callback);
1197                         }
1198                     });
1199         } catch (RemoteException e) {
1200             throw e.rethrowFromSystemServer();
1201         }
1202     }
1203 
1204     /**
1205      * API to read records based on {@link ReadRecordsRequestUsingFilters} or {@link
1206      * ReadRecordsRequestUsingIds}
1207      *
1208      * <p>Number of records returned by this API will depend based on below factors:
1209      *
1210      * <p>When an app with read permission allowed calls the API from background then it will be
1211      * able to read only its own inserted records and will not get records inserted by other apps.
1212      * This may be less than the total records present for the record type.
1213      *
1214      * <p>When an app with read permission allowed calls the API from foreground then it will be
1215      * able to read all records for the record type.
1216      *
1217      * <p>App with only write permission but no read permission allowed will be able to read only
1218      * its own inserted records both when in foreground or background.
1219      *
1220      * <p>An app without both read and write permissions will not be able to read any record and the
1221      * API will throw Security Exception.
1222      *
1223      * @param request Read request based on {@link ReadRecordsRequestUsingFilters} or {@link
1224      *     ReadRecordsRequestUsingIds}
1225      * @param executor Executor on which to invoke the callback.
1226      * @param callback Callback to receive result of performing this operation.
1227      * @throws IllegalArgumentException if request page size set is more than 5000 in {@link
1228      *     ReadRecordsRequestUsingFilters}
1229      * @throws SecurityException if app without read or write permission tries to read.
1230      */
readRecords( @onNull ReadRecordsRequest<T> request, @NonNull Executor executor, @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback)1231     public <T extends Record> void readRecords(
1232             @NonNull ReadRecordsRequest<T> request,
1233             @NonNull Executor executor,
1234             @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
1235         Objects.requireNonNull(request);
1236         Objects.requireNonNull(executor);
1237         Objects.requireNonNull(callback);
1238         try {
1239             mService.readRecords(
1240                     mContext.getAttributionSource(),
1241                     request.toReadRecordsRequestParcel(),
1242                     getReadCallback(executor, callback));
1243         } catch (RemoteException remoteException) {
1244             remoteException.rethrowFromSystemServer();
1245         }
1246     }
1247 
1248     /**
1249      * Updates {@code records} into the HealthConnect database. In case of an error or a permission
1250      * failure the HealthConnect service, {@link OutcomeReceiver#onError} will be invoked with a
1251      * {@link HealthConnectException}.
1252      *
1253      * <p>In case the input record to be updated does not exist in the database or the caller is not
1254      * the owner of the record then {@link HealthConnectException#ERROR_INVALID_ARGUMENT} will be
1255      * thrown.
1256      *
1257      * @param records list of records to be updated.
1258      * @param executor Executor on which to invoke the callback.
1259      * @param callback Callback to receive result of performing this operation.
1260      * @throws IllegalArgumentException if at least one of the records is missing both
1261      *     ClientRecordID and UUID.
1262      */
updateRecords( @onNull List<Record> records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1263     public void updateRecords(
1264             @NonNull List<Record> records,
1265             @NonNull @CallbackExecutor Executor executor,
1266             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1267         Objects.requireNonNull(records);
1268         Objects.requireNonNull(executor);
1269         Objects.requireNonNull(callback);
1270         try {
1271             List<RecordInternal<?>> recordInternals =
1272                     records.stream().map(Record::toRecordInternal).collect(Collectors.toList());
1273             // Verify if the input record has clientRecordId or UUID.
1274             for (RecordInternal<?> recordInternal : recordInternals) {
1275                 if ((recordInternal.getClientRecordId() == null
1276                                 || recordInternal.getClientRecordId().isEmpty())
1277                         && recordInternal.getUuid() == null) {
1278                     throw new IllegalArgumentException(
1279                             "At least one of the records is missing both ClientRecordID"
1280                                     + " and UUID. RecordType of the input: "
1281                                     + recordInternal.getRecordType());
1282                 }
1283             }
1284 
1285             mService.updateRecords(
1286                     mContext.getAttributionSource(),
1287                     new RecordsParcel(recordInternals),
1288                     new IEmptyResponseCallback.Stub() {
1289                         @Override
1290                         public void onResult() {
1291                             Binder.clearCallingIdentity();
1292                             executor.execute(() -> callback.onResult(null));
1293                         }
1294 
1295                         @Override
1296                         public void onError(HealthConnectExceptionParcel exception) {
1297                             Binder.clearCallingIdentity();
1298                             callback.onError(exception.getHealthConnectException());
1299                         }
1300                     });
1301         } catch (ArithmeticException
1302                 | ClassCastException
1303                 | IllegalArgumentException invalidArgumentException) {
1304             throw new IllegalArgumentException(invalidArgumentException);
1305         } catch (RemoteException e) {
1306             throw e.rethrowFromSystemServer();
1307         }
1308     }
1309 
1310     /**
1311      * Returns information, represented by {@code ApplicationInfoResponse}, for all the packages
1312      * that have contributed to the health connect DB. If the application is does not have
1313      * permissions to query other packages, a {@link java.lang.SecurityException} is thrown.
1314      *
1315      * @param executor Executor on which to invoke the callback.
1316      * @param callback Callback to receive result of performing this operation.
1317      * @hide
1318      */
1319     @SystemApi
1320     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getContributorApplicationsInfo( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback)1321     public void getContributorApplicationsInfo(
1322             @NonNull @CallbackExecutor Executor executor,
1323             @NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback) {
1324         Objects.requireNonNull(executor);
1325         Objects.requireNonNull(callback);
1326 
1327         try {
1328             mService.getContributorApplicationsInfo(
1329                     new IApplicationInfoResponseCallback.Stub() {
1330                         @Override
1331                         public void onResult(ApplicationInfoResponseParcel parcel) {
1332                             Binder.clearCallingIdentity();
1333                             executor.execute(
1334                                     () ->
1335                                             callback.onResult(
1336                                                     new ApplicationInfoResponse(
1337                                                             parcel.getAppInfoList())));
1338                         }
1339 
1340                         @Override
1341                         public void onError(HealthConnectExceptionParcel exception) {
1342                             returnError(executor, exception, callback);
1343                         }
1344                     });
1345 
1346         } catch (RemoteException e) {
1347             throw e.rethrowFromSystemServer();
1348         }
1349     }
1350 
1351     /**
1352      * Stages all HealthConnect remote data and returns any errors in a callback. Errors encountered
1353      * for all the files are shared in the provided callback. Any authorization / permissions
1354      * related error is reported to the callback with an empty file name.
1355      *
1356      * <p>The staged data will later be restored (integrated) into the existing Health Connect data.
1357      * Any existing data will not be affected by the staged data.
1358      *
1359      * <p>The file names passed should be the same as the ones on the original device that were
1360      * backed up or are being transferred directly.
1361      *
1362      * <p>If a file already exists in the staged data then it will be replaced. However, note that
1363      * staging data is a one time process. And if the staged data has already been processed then
1364      * any attempt to stage data again will be silently ignored.
1365      *
1366      * <p>The caller is responsible for closing the original file descriptors. The file descriptors
1367      * are duplicated and the originals may be closed by the application at any time after this API
1368      * returns.
1369      *
1370      * <p>The caller should update the data download states using {@link #updateDataDownloadState}
1371      * before calling this API.
1372      *
1373      * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s.
1374      * @param executor The {@link Executor} on which to invoke the callback.
1375      * @param callback The callback which will receive the outcome of this call.
1376      * @hide
1377      */
1378     @SystemApi
1379     @UserHandleAware
1380     @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA)
stageAllHealthConnectRemoteData( @onNull Map<String, ParcelFileDescriptor> pfdsByFileName, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, StageRemoteDataException> callback)1381     public void stageAllHealthConnectRemoteData(
1382             @NonNull Map<String, ParcelFileDescriptor> pfdsByFileName,
1383             @NonNull Executor executor,
1384             @NonNull OutcomeReceiver<Void, StageRemoteDataException> callback)
1385             throws NullPointerException {
1386         Objects.requireNonNull(pfdsByFileName);
1387         Objects.requireNonNull(executor);
1388         Objects.requireNonNull(callback);
1389 
1390         try {
1391             mService.stageAllHealthConnectRemoteData(
1392                     new StageRemoteDataRequest(pfdsByFileName),
1393                     mContext.getUser(),
1394                     new IDataStagingFinishedCallback.Stub() {
1395                         @Override
1396                         public void onResult() {
1397                             Binder.clearCallingIdentity();
1398                             executor.execute(() -> callback.onResult(null));
1399                         }
1400 
1401                         @Override
1402                         public void onError(StageRemoteDataException stageRemoteDataException) {
1403                             Binder.clearCallingIdentity();
1404                             executor.execute(() -> callback.onError(stageRemoteDataException));
1405                         }
1406                     });
1407         } catch (RemoteException e) {
1408             throw e.rethrowFromSystemServer();
1409         }
1410     }
1411 
1412     /**
1413      * Copies all HealthConnect backup data in the passed FDs.
1414      *
1415      * <p>The shared data must later be sent for Backup to cloud or another device.
1416      *
1417      * <p>We are responsible for closing the original file descriptors. The caller must not close
1418      * the FD before that.
1419      *
1420      * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s.
1421      * @hide
1422      */
getAllDataForBackup(@onNull Map<String, ParcelFileDescriptor> pfdsByFileName)1423     public void getAllDataForBackup(@NonNull Map<String, ParcelFileDescriptor> pfdsByFileName) {
1424         Objects.requireNonNull(pfdsByFileName);
1425 
1426         try {
1427             mService.getAllDataForBackup(
1428                     new StageRemoteDataRequest(pfdsByFileName), mContext.getUser());
1429         } catch (RemoteException e) {
1430             throw e.rethrowFromSystemServer();
1431         }
1432     }
1433 
1434     /**
1435      * Returns the names of all HealthConnect Backup files
1436      *
1437      * @hide
1438      */
getAllBackupFileNames(boolean forDeviceToDevice)1439     public Set<String> getAllBackupFileNames(boolean forDeviceToDevice) {
1440         try {
1441             return mService.getAllBackupFileNames(forDeviceToDevice).getFileNames();
1442         } catch (RemoteException e) {
1443             throw e.rethrowFromSystemServer();
1444         }
1445     }
1446 
1447     /**
1448      * Deletes all previously staged HealthConnect data from the disk. For testing purposes only.
1449      *
1450      * <p>This deletes only the staged data leaving any other Health Connect data untouched.
1451      *
1452      * @hide
1453      */
1454     @TestApi
1455     @UserHandleAware
deleteAllStagedRemoteData()1456     public void deleteAllStagedRemoteData() throws NullPointerException {
1457         try {
1458             mService.deleteAllStagedRemoteData(mContext.getUser());
1459         } catch (RemoteException e) {
1460             throw e.rethrowFromSystemServer();
1461         }
1462     }
1463 
1464     /**
1465      * Allows setting lower rate limits in tests.
1466      *
1467      * @hide
1468      */
1469     @TestApi
setLowerRateLimitsForTesting(boolean enabled)1470     public void setLowerRateLimitsForTesting(boolean enabled) throws NullPointerException {
1471         try {
1472             mService.setLowerRateLimitsForTesting(enabled);
1473         } catch (RemoteException e) {
1474             throw e.rethrowFromSystemServer();
1475         }
1476     }
1477 
1478     /**
1479      * Updates the download state of the Health Connect data.
1480      *
1481      * <p>The data should've been downloaded and the corresponding download states updated before
1482      * the app calls {@link #stageAllHealthConnectRemoteData}. Once {@link
1483      * #stageAllHealthConnectRemoteData} has been called the downloaded state becomes {@link
1484      * #DATA_DOWNLOAD_COMPLETE} and future attempts to update the download state are ignored.
1485      *
1486      * <p>The only valid order of state transition are:
1487      *
1488      * <ul>
1489      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_COMPLETE}
1490      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
1491      *       #DATA_DOWNLOAD_COMPLETE}
1492      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_FAILED}
1493      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
1494      *       #DATA_DOWNLOAD_FAILED}
1495      * </ul>
1496      *
1497      * <p>Note that it's okay if some states are missing in of the sequences above but the order has
1498      * to be one of the above.
1499      *
1500      * <p>Only one app will have the permission to call this API so it is assured that no one else
1501      * will be able to update this state.
1502      *
1503      * @param downloadState The download state which needs to be purely from {@link
1504      *     DataDownloadState}
1505      * @hide
1506      */
1507     @SystemApi
1508     @UserHandleAware
1509     @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA)
updateDataDownloadState(@ataDownloadState int downloadState)1510     public void updateDataDownloadState(@DataDownloadState int downloadState) {
1511         try {
1512             mService.updateDataDownloadState(downloadState);
1513         } catch (RemoteException e) {
1514             throw e.rethrowFromSystemServer();
1515         }
1516     }
1517 
1518     /**
1519      * Asynchronously returns the current UI state of Health Connect as it goes through the
1520      * Data-Migration process. In case there was an error reading the data on the disk the error
1521      * will be returned in the callback.
1522      *
1523      * <p>See also {@link HealthConnectMigrationUiState} object describing the HealthConnect UI
1524      * state.
1525      *
1526      * @param executor The {@link Executor} on which to invoke the callback.
1527      * @param callback The callback which will receive the current {@link
1528      *     HealthConnectMigrationUiState} or the {@link HealthConnectException}.
1529      * @hide
1530      */
1531     @UserHandleAware
1532     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getHealthConnectMigrationUiState( @onNull Executor executor, @NonNull OutcomeReceiver<HealthConnectMigrationUiState, HealthConnectException> callback)1533     public void getHealthConnectMigrationUiState(
1534             @NonNull Executor executor,
1535             @NonNull
1536                     OutcomeReceiver<HealthConnectMigrationUiState, HealthConnectException>
1537                             callback) {
1538         Objects.requireNonNull(executor);
1539         Objects.requireNonNull(callback);
1540 
1541         try {
1542             mService.getHealthConnectMigrationUiState(
1543                     new IGetHealthConnectMigrationUiStateCallback.Stub() {
1544                         @Override
1545                         public void onResult(HealthConnectMigrationUiState migrationUiState) {
1546                             Binder.clearCallingIdentity();
1547                             executor.execute(() -> callback.onResult(migrationUiState));
1548                         }
1549 
1550                         @Override
1551                         public void onError(HealthConnectExceptionParcel exception) {
1552                             Binder.clearCallingIdentity();
1553                             executor.execute(
1554                                     () -> callback.onError(exception.getHealthConnectException()));
1555                         }
1556                     });
1557         } catch (RemoteException e) {
1558             throw e.rethrowFromSystemServer();
1559         }
1560     }
1561 
1562     /**
1563      * Asynchronously returns the current state of the Health Connect data as it goes through the
1564      * Data-Restore and/or the Data-Migration process. In case there was an error reading the data
1565      * on the disk the error will be returned in the callback.
1566      *
1567      * <p>See also {@link HealthConnectDataState} object describing the HealthConnect state.
1568      *
1569      * @param executor The {@link Executor} on which to invoke the callback.
1570      * @param callback The callback which will receive the current {@link HealthConnectDataState} or
1571      *     the {@link HealthConnectException}.
1572      * @hide
1573      */
1574     @SystemApi
1575     @UserHandleAware
1576     @RequiresPermission(
1577             anyOf = {
1578                 MANAGE_HEALTH_DATA_PERMISSION,
1579                 Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA
1580             })
getHealthConnectDataState( @onNull Executor executor, @NonNull OutcomeReceiver<HealthConnectDataState, HealthConnectException> callback)1581     public void getHealthConnectDataState(
1582             @NonNull Executor executor,
1583             @NonNull OutcomeReceiver<HealthConnectDataState, HealthConnectException> callback) {
1584         Objects.requireNonNull(executor);
1585         Objects.requireNonNull(callback);
1586         try {
1587             mService.getHealthConnectDataState(
1588                     new IGetHealthConnectDataStateCallback.Stub() {
1589                         @Override
1590                         public void onResult(HealthConnectDataState healthConnectDataState) {
1591                             Binder.clearCallingIdentity();
1592                             executor.execute(() -> callback.onResult(healthConnectDataState));
1593                         }
1594 
1595                         @Override
1596                         public void onError(HealthConnectExceptionParcel exception) {
1597                             Binder.clearCallingIdentity();
1598                             executor.execute(
1599                                     () -> callback.onError(exception.getHealthConnectException()));
1600                         }
1601                     });
1602         } catch (RemoteException e) {
1603             throw e.rethrowFromSystemServer();
1604         }
1605     }
1606 
1607     /**
1608      * Returns a list of unique dates for which the DB has at least one entry.
1609      *
1610      * @param recordTypes List of record types classes for which to get the activity dates.
1611      * @param executor Executor on which to invoke the callback.
1612      * @param callback Callback to receive result of performing this operation.
1613      * @throws java.lang.IllegalArgumentException If the record types list is empty.
1614      * @hide
1615      */
1616     @SystemApi
1617     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryActivityDates( @onNull List<Class<? extends Record>> recordTypes, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<List<LocalDate>, HealthConnectException> callback)1618     public void queryActivityDates(
1619             @NonNull List<Class<? extends Record>> recordTypes,
1620             @NonNull @CallbackExecutor Executor executor,
1621             @NonNull OutcomeReceiver<List<LocalDate>, HealthConnectException> callback) {
1622         Objects.requireNonNull(executor);
1623         Objects.requireNonNull(callback);
1624         Objects.requireNonNull(recordTypes);
1625 
1626         if (recordTypes.isEmpty()) {
1627             throw new IllegalArgumentException("Record types list can not be empty");
1628         }
1629 
1630         try {
1631             mService.getActivityDates(
1632                     new ActivityDatesRequestParcel(recordTypes),
1633                     new IActivityDatesResponseCallback.Stub() {
1634                         @Override
1635                         public void onResult(ActivityDatesResponseParcel parcel) {
1636                             Binder.clearCallingIdentity();
1637                             executor.execute(() -> callback.onResult(parcel.getDates()));
1638                         }
1639 
1640                         @Override
1641                         public void onError(HealthConnectExceptionParcel exception) {
1642                             returnError(executor, exception, callback);
1643                         }
1644                     });
1645 
1646         } catch (RemoteException exception) {
1647             exception.rethrowFromSystemServer();
1648         }
1649     }
1650 
1651     /**
1652      * Marks the start of the migration and block API calls.
1653      *
1654      * @param executor Executor on which to invoke the callback.
1655      * @param callback Callback to receive result of performing this operation.
1656      * @hide
1657      */
1658     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1659     @SystemApi
startMigration( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1660     public void startMigration(
1661             @NonNull @CallbackExecutor Executor executor,
1662             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1663         Objects.requireNonNull(executor);
1664         Objects.requireNonNull(callback);
1665         try {
1666             mService.startMigration(
1667                     mContext.getPackageName(), wrapMigrationCallback(executor, callback));
1668         } catch (RemoteException e) {
1669             throw e.rethrowFromSystemServer();
1670         }
1671     }
1672 
1673     /**
1674      * Marks the end of the migration.
1675      *
1676      * @param executor Executor on which to invoke the callback.
1677      * @param callback Callback to receive result of performing this operation.
1678      * @hide
1679      */
1680     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1681     @SystemApi
finishMigration( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1682     public void finishMigration(
1683             @NonNull @CallbackExecutor Executor executor,
1684             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1685         Objects.requireNonNull(executor);
1686         Objects.requireNonNull(callback);
1687         try {
1688             mService.finishMigration(
1689                     mContext.getPackageName(), wrapMigrationCallback(executor, callback));
1690         } catch (RemoteException e) {
1691             throw e.rethrowFromSystemServer();
1692         }
1693     }
1694 
1695     /**
1696      * Writes data to the module database.
1697      *
1698      * @param entities List of {@link MigrationEntity} to migrate.
1699      * @param executor Executor on which to invoke the callback.
1700      * @param callback Callback to receive result of performing this operation.
1701      * @hide
1702      */
1703     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1704     @SystemApi
writeMigrationData( @onNull List<MigrationEntity> entities, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1705     public void writeMigrationData(
1706             @NonNull List<MigrationEntity> entities,
1707             @NonNull @CallbackExecutor Executor executor,
1708             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1709 
1710         Objects.requireNonNull(entities);
1711         Objects.requireNonNull(executor);
1712         Objects.requireNonNull(callback);
1713 
1714         try {
1715             mService.writeMigrationData(
1716                     mContext.getPackageName(),
1717                     new MigrationEntityParcel(entities),
1718                     wrapMigrationCallback(executor, callback));
1719         } catch (RemoteException e) {
1720             throw e.rethrowFromSystemServer();
1721         }
1722     }
1723 
1724     /**
1725      * Sets the minimum version on which the module will inform the migrator package of its
1726      * migration readiness.
1727      *
1728      * @param executor Executor on which to invoke the callback.
1729      * @param callback Callback to receive result of performing this operation.
1730      * @hide
1731      */
1732     @SystemApi
1733     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
insertMinDataMigrationSdkExtensionVersion( int requiredSdkExtension, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1734     public void insertMinDataMigrationSdkExtensionVersion(
1735             int requiredSdkExtension,
1736             @NonNull @CallbackExecutor Executor executor,
1737             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1738         Objects.requireNonNull(executor);
1739         Objects.requireNonNull(callback);
1740         try {
1741             mService.insertMinDataMigrationSdkExtensionVersion(
1742                     mContext.getPackageName(),
1743                     requiredSdkExtension,
1744                     wrapMigrationCallback(executor, callback));
1745 
1746         } catch (RemoteException e) {
1747             throw e.rethrowFromSystemServer();
1748         }
1749     }
1750 
1751     /**
1752      * Configures the settings for the scheduled export of Health Connect data.
1753      *
1754      * @param settings Settings to use for the scheduled export. Use null to clear the settings.
1755      * @throws RuntimeException for internal errors
1756      * @hide
1757      */
1758     @SuppressWarnings("NullAway") // TODO: b/178748627 - fix this suppression.
1759     @WorkerThread
1760     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
configureScheduledExport(@ullable ScheduledExportSettings settings)1761     public void configureScheduledExport(@Nullable ScheduledExportSettings settings) {
1762         try {
1763             mService.configureScheduledExport(settings, mContext.getUser());
1764         } catch (RemoteException e) {
1765             e.rethrowFromSystemServer();
1766         }
1767     }
1768 
1769     /**
1770      * Queries the status of a scheduled export.
1771      *
1772      * @throws RuntimeException for internal errors
1773      * @hide
1774      */
1775     @WorkerThread
1776     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getScheduledExportStatus( @onNull Executor executor, @NonNull OutcomeReceiver<ScheduledExportStatus, HealthConnectException> callback)1777     public void getScheduledExportStatus(
1778             @NonNull Executor executor,
1779             @NonNull OutcomeReceiver<ScheduledExportStatus, HealthConnectException> callback) {
1780         Objects.requireNonNull(executor);
1781         Objects.requireNonNull(callback);
1782 
1783         try {
1784             mService.getScheduledExportStatus(
1785                     mContext.getUser(),
1786                     new IScheduledExportStatusCallback.Stub() {
1787                         @Override
1788                         public void onResult(ScheduledExportStatus status) {
1789                             Binder.clearCallingIdentity();
1790                             executor.execute(() -> callback.onResult(status));
1791                         }
1792 
1793                         @Override
1794                         public void onError(HealthConnectExceptionParcel exception) {
1795                             returnError(executor, exception, callback);
1796                         }
1797                     });
1798         } catch (RemoteException e) {
1799             e.rethrowFromSystemServer();
1800         }
1801     }
1802 
1803     /**
1804      * Queries the status of a data import.
1805      *
1806      * @throws RuntimeException for internal errors
1807      * @hide
1808      */
1809     @WorkerThread
1810     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getImportStatus( @onNull Executor executor, @NonNull OutcomeReceiver<ImportStatus, HealthConnectException> callback)1811     public void getImportStatus(
1812             @NonNull Executor executor,
1813             @NonNull OutcomeReceiver<ImportStatus, HealthConnectException> callback) {
1814         Objects.requireNonNull(executor);
1815         Objects.requireNonNull(callback);
1816 
1817         try {
1818             mService.getImportStatus(
1819                     mContext.getUser(),
1820                     new IImportStatusCallback.Stub() {
1821                         @Override
1822                         public void onResult(ImportStatus status) {
1823                             Binder.clearCallingIdentity();
1824                             executor.execute(() -> callback.onResult(status));
1825                         }
1826 
1827                         @Override
1828                         public void onError(HealthConnectExceptionParcel exception) {
1829                             returnError(executor, exception, callback);
1830                         }
1831                     });
1832         } catch (RemoteException e) {
1833             e.rethrowFromSystemServer();
1834         }
1835     }
1836 
1837     /**
1838      * Imports the given compressed database file.
1839      *
1840      * @throws RuntimeException for internal errors
1841      * @hide
1842      */
1843     @WorkerThread
1844     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
runImport( @onNull Uri file, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1845     public void runImport(
1846             @NonNull Uri file,
1847             @NonNull Executor executor,
1848             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1849         Objects.requireNonNull(file);
1850         Objects.requireNonNull(executor);
1851         Objects.requireNonNull(callback);
1852         try {
1853             mService.runImport(
1854                     mContext.getUser(),
1855                     file,
1856                     new IEmptyResponseCallback.Stub() {
1857                         @Override
1858                         public void onResult() {
1859                             Binder.clearCallingIdentity();
1860                             executor.execute(() -> callback.onResult(null));
1861                         }
1862 
1863                         @Override
1864                         public void onError(HealthConnectExceptionParcel exception) {
1865                             returnError(executor, exception, callback);
1866                         }
1867                     });
1868         } catch (RemoteException e) {
1869             e.rethrowFromSystemServer();
1870         }
1871     }
1872 
1873     /**
1874      * Triggers an immediate export of health connect data.
1875      *
1876      * @throws RuntimeException for internal errors
1877      * @hide
1878      */
1879     @WorkerThread
1880     @FlaggedApi(FLAG_IMMEDIATE_EXPORT)
1881     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
runImmediateExport( @onNull Uri file, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1882     public void runImmediateExport(
1883             @NonNull Uri file,
1884             @NonNull Executor executor,
1885             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1886         Objects.requireNonNull(file);
1887         Objects.requireNonNull(executor);
1888         Objects.requireNonNull(callback);
1889         try {
1890             mService.runImmediateExport(
1891                     file,
1892                     new IEmptyResponseCallback.Stub() {
1893                         @Override
1894                         public void onResult() {
1895                             Binder.clearCallingIdentity();
1896                             executor.execute(() -> callback.onResult(null));
1897                         }
1898 
1899                         @Override
1900                         public void onError(HealthConnectExceptionParcel exception) {
1901                             returnError(executor, exception, callback);
1902                         }
1903                     });
1904         } catch (RemoteException e) {
1905             e.rethrowFromSystemServer();
1906         }
1907     }
1908 
1909     /**
1910      * Returns currently set period between scheduled exports for this user.
1911      *
1912      * <p>If you are calling this function for the first time after a user unlock, this might take
1913      * some time so consider calling this on a thread.
1914      *
1915      * @return Period between scheduled exports in days, 0 is returned if period between scheduled
1916      *     exports is not set.
1917      * @throws RuntimeException for internal errors
1918      * @hide
1919      */
1920     @WorkerThread
1921     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
1922     @IntRange(from = 0, to = 30)
getScheduledExportPeriodInDays()1923     public int getScheduledExportPeriodInDays() {
1924         try {
1925             return mService.getScheduledExportPeriodInDays(mContext.getUser());
1926         } catch (RemoteException e) {
1927             throw e.rethrowFromSystemServer();
1928         }
1929     }
1930 
1931     /**
1932      * Queries the document providers available to be used for export/import.
1933      *
1934      * @throws RuntimeException for internal errors
1935      * @hide
1936      */
1937     @WorkerThread
1938     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryDocumentProviders( @onNull Executor executor, @NonNull OutcomeReceiver<List<ExportImportDocumentProvider>, HealthConnectException> callback)1939     public void queryDocumentProviders(
1940             @NonNull Executor executor,
1941             @NonNull
1942                     OutcomeReceiver<List<ExportImportDocumentProvider>, HealthConnectException>
1943                             callback) {
1944         Objects.requireNonNull(executor);
1945         Objects.requireNonNull(callback);
1946 
1947         try {
1948             mService.queryDocumentProviders(
1949                     mContext.getUser(),
1950                     new IQueryDocumentProvidersCallback.Stub() {
1951                         @Override
1952                         public void onResult(List<ExportImportDocumentProvider> providers) {
1953                             Binder.clearCallingIdentity();
1954                             executor.execute(() -> callback.onResult(providers));
1955                         }
1956 
1957                         @Override
1958                         public void onError(HealthConnectExceptionParcel exception) {
1959                             returnError(executor, exception, callback);
1960                         }
1961                     });
1962         } catch (RemoteException e) {
1963             e.rethrowFromSystemServer();
1964         }
1965     }
1966 
1967     @SuppressWarnings("unchecked")
getReadCallback( @onNull Executor executor, @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback)1968     private <T extends Record> IReadRecordsResponseCallback.Stub getReadCallback(
1969             @NonNull Executor executor,
1970             @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
1971         return new IReadRecordsResponseCallback.Stub() {
1972             @Override
1973             public void onResult(ReadRecordsResponseParcel parcel) {
1974                 Binder.clearCallingIdentity();
1975                 try {
1976                     List<T> externalRecords =
1977                             (List<T>)
1978                                     mInternalExternalRecordConverter.getExternalRecords(
1979                                             parcel.getRecordsParcel().getRecords());
1980                     executor.execute(
1981                             () ->
1982                                     callback.onResult(
1983                                             new ReadRecordsResponse<>(
1984                                                     externalRecords, parcel.getPageToken())));
1985                 } catch (ClassCastException castException) {
1986                     HealthConnectException healthConnectException =
1987                             new HealthConnectException(
1988                                     HealthConnectException.ERROR_INTERNAL,
1989                                     castException.getMessage());
1990                     returnError(
1991                             executor,
1992                             new HealthConnectExceptionParcel(healthConnectException),
1993                             callback);
1994                 }
1995             }
1996 
1997             @Override
1998             public void onError(HealthConnectExceptionParcel exception) {
1999                 returnError(executor, exception, callback);
2000             }
2001         };
2002     }
2003 
2004     private List<Record> toExternalRecordsWithUuids(
2005             List<RecordInternal<?>> recordInternals, List<String> uuids) {
2006         int i = 0;
2007         List<Record> records = new ArrayList<>();
2008 
2009         for (RecordInternal<?> recordInternal : recordInternals) {
2010             recordInternal.setUuid(uuids.get(i++));
2011             records.add(recordInternal.toExternalRecord());
2012         }
2013 
2014         return records;
2015     }
2016 
2017     private static <RES, ERR extends Throwable> void returnResult(
2018             Executor executor, @Nullable RES result, OutcomeReceiver<RES, ERR> callback) {
2019         Binder.clearCallingIdentity();
2020         executor.execute(() -> callback.onResult(result));
2021     }
2022 
2023     private void returnError(
2024             Executor executor,
2025             HealthConnectExceptionParcel exception,
2026             OutcomeReceiver<?, HealthConnectException> callback) {
2027         Binder.clearCallingIdentity();
2028         executor.execute(() -> callback.onError(exception.getHealthConnectException()));
2029     }
2030 
2031     /** @hide */
2032     @Retention(RetentionPolicy.SOURCE)
2033     @IntDef({
2034         DATA_DOWNLOAD_STATE_UNKNOWN,
2035         DATA_DOWNLOAD_STARTED,
2036         DATA_DOWNLOAD_RETRY,
2037         DATA_DOWNLOAD_FAILED,
2038         DATA_DOWNLOAD_COMPLETE
2039     })
2040     public @interface DataDownloadState {}
2041 
2042     /**
2043      * Returns {@code true} if the given permission protects access to health connect data.
2044      *
2045      * @hide
2046      */
2047     @SystemApi
2048     public static boolean isHealthPermission(
2049             @NonNull Context context, @NonNull final String permission) {
2050         if (!permission.startsWith(HEALTH_PERMISSION_PREFIX)) {
2051             return false;
2052         }
2053         return getHealthPermissions(context).contains(permission);
2054     }
2055 
2056     /**
2057      * Returns an <b>immutable</b> set of health permissions defined within the module and belonging
2058      * to {@link android.health.connect.HealthPermissions#HEALTH_PERMISSION_GROUP}.
2059      *
2060      * <p><b>Note:</b> If we, for some reason, fail to retrieve these, we return an empty set rather
2061      * than crashing the device. This means the health permissions infra will be inactive.
2062      *
2063      * @hide
2064      */
2065     @NonNull
2066     @SystemApi
2067     public static Set<String> getHealthPermissions(@NonNull Context context) {
2068         if (sHealthPermissions != null) {
2069             return sHealthPermissions;
2070         }
2071 
2072         PackageInfo packageInfo;
2073         try {
2074             final PackageManager pm = context.getApplicationContext().getPackageManager();
2075             final PermissionGroupInfo permGroupInfo =
2076                     pm.getPermissionGroupInfo(
2077                             android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP,
2078                             /* flags= */ 0);
2079             packageInfo =
2080                     pm.getPackageInfo(
2081                             permGroupInfo.packageName,
2082                             PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
2083         } catch (PackageManager.NameNotFoundException ex) {
2084             Log.e(TAG, "Health permission group or HC package not found", ex);
2085             Set<String> result = Collections.emptySet();
2086             sHealthPermissions = result;
2087             return result;
2088         }
2089 
2090         Set<String> permissions = new HashSet<>();
2091         for (PermissionInfo perm : packageInfo.permissions) {
2092             if (HealthPermissions.isValidHealthPermission(perm)) {
2093                 permissions.add(perm.name);
2094             }
2095         }
2096         Set<String> result = Collections.unmodifiableSet(permissions);
2097         sHealthPermissions = result;
2098         return result;
2099     }
2100 
2101     /**
2102      * Allow tests to reset the cached Set of Health Connect permissions.
2103      *
2104      * @hide
2105      */
2106     @VisibleForTesting
2107     public static void resetHealthPermissionsCache() {
2108         // This method should only be used for testing. Having a global static cache of Health
2109         // Connect permissions is good for efficiency but can play havoc with test isolation.
2110         // Allow tests which modify the package manager to clear the cache.
2111         sHealthPermissions = null;
2112     }
2113 
2114     @NonNull
2115     private static IMigrationCallback wrapMigrationCallback(
2116             @NonNull @CallbackExecutor Executor executor,
2117             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
2118         return new IMigrationCallback.Stub() {
2119             @Override
2120             public void onSuccess() {
2121                 Binder.clearCallingIdentity();
2122                 executor.execute(() -> callback.onResult(null));
2123             }
2124 
2125             @Override
2126             public void onError(MigrationException exception) {
2127                 Binder.clearCallingIdentity();
2128                 executor.execute(() -> callback.onError(exception));
2129             }
2130         };
2131     }
2132 
2133     /**
2134      * Inserts or updates a list of {@link MedicalResource}s into the HealthConnect database using
2135      * {@link UpsertMedicalResourceRequest}.
2136      *
2137      * <p>For each {@link UpsertMedicalResourceRequest}, one {@link MedicalResource} will be
2138      * returned. The returned list of {@link MedicalResource}s will be in the same order as the
2139      * {@code requests}.
2140      *
2141      * <p>Note that a {@link MedicalDataSource} needs to be created using {@link
2142      * #createMedicalDataSource} before any {@link MedicalResource}s can be upserted for this
2143      * source.
2144      *
2145      * <p>Medical data is represented using the <a href="https://hl7.org/fhir/">Fast Healthcare
2146      * Interoperability Resources (FHIR)</a> standard. The FHIR resource provided in {@link
2147      * UpsertMedicalResourceRequest#getData()} is expected to be valid FHIR in JSON representation
2148      * for the specified {@link UpsertMedicalResourceRequest#getFhirVersion()} according to the <a
2149      * href="https://hl7.org/fhir/resourcelist.html">FHIR spec</a>. Structural validation checks
2150      * such as resource structure, field types and presence of required fields are performed, but
2151      * these checks may not cover all FHIR spec requirements and may change in future versions.
2152      *
2153      * <p>Data written to Health Connect should be for a single individual only. However, the API
2154      * allows for multiple Patient resources to be written to account for the possibility of
2155      * multiple Patient resources being present in one individual's medical record.
2156      *
2157      * <p>Each {@link UpsertMedicalResourceRequest} also has to meet the following requirements.
2158      *
2159      * <ul>
2160      *   <li>The FHIR resource contains an "id" and "resourceType" field.
2161      *   <li>The FHIR resource type is in our accepted list of resource types. See {@link
2162      *       FhirResource} for the accepted types.
2163      *   <li>The FHIR resource does not contain any "contained" resources.
2164      *   <li>The resource can be mapped to one of the READ_MEDICAL_DATA_ {@link HealthPermissions}
2165      *       categories.
2166      *   <li>The {@link UpsertMedicalResourceRequest#getDataSourceId()} is valid.
2167      *   <li>The {@link UpsertMedicalResourceRequest#getFhirVersion()} matches the {@link
2168      *       FhirVersion} of the {@link MedicalDataSource}.
2169      * </ul>
2170      *
2171      * <p>If any request contains invalid {@link MedicalDataSource} IDs, the API will throw an
2172      * {@link IllegalArgumentException}, and none of the {@code requests} will be upserted into the
2173      * HealthConnect database.
2174      *
2175      * <p>If any request is deemed invalid for any other reasons, the caller will receive an
2176      * exception with code {@link HealthConnectException#ERROR_INVALID_ARGUMENT} via {@code
2177      * callback.onError()}, and none of the {@code requests} will be upserted into the HealthConnect
2178      * database.
2179      *
2180      * <p>If data for any {@link UpsertMedicalResourceRequest} fails to be upserted, then no data
2181      * from any {@code requests} will be upserted into the database.
2182      *
2183      * <p>The uniqueness of each request is calculated comparing the combination of {@link
2184      * UpsertMedicalResourceRequest#getDataSourceId() data source id}, FHIR resource type and FHIR
2185      * resource ID extracted from the provided {@link UpsertMedicalResourceRequest#getData() data}.
2186      * If the above combination does not match with an existing one in Health Connect, then a new
2187      * {@link MedicalResource} is inserted, otherwise the existing one is updated.
2188      *
2189      * @param requests List of upsert requests.
2190      * @param executor Executor on which to invoke the callback.
2191      * @param callback Callback to receive result of performing this operation.
2192      * @throws IllegalArgumentException if any {@code requests} contains invalid {@link
2193      *     MedicalDataSource} IDs.
2194      */
2195     // Suppress missing because API flagged out.
2196     // TODO: b/355156275 - remove suppression once API not flagged out.
2197     @SuppressWarnings({"MissingPermission"})
2198     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2199     @RequiresPermission(WRITE_MEDICAL_DATA)
2200     public void upsertMedicalResources(
2201             @NonNull List<UpsertMedicalResourceRequest> requests,
2202             @NonNull @CallbackExecutor Executor executor,
2203             @NonNull OutcomeReceiver<List<MedicalResource>, HealthConnectException> callback) {
2204         Objects.requireNonNull(requests);
2205         Objects.requireNonNull(executor);
2206         Objects.requireNonNull(callback);
2207 
2208         if (requests.isEmpty()) {
2209             returnResult(executor, List.of(), callback);
2210             return;
2211         }
2212 
2213         try {
2214             if (Flags.phrUpsertFixUseSharedMemory()) {
2215                 mService.upsertMedicalResourcesFromRequestsParcel(
2216                         mContext.getAttributionSource(),
2217                         new UpsertMedicalResourceRequestsParcel(requests),
2218                         new IMedicalResourceListParcelResponseCallback.Stub() {
2219                             @Override
2220                             public void onResult(
2221                                     MedicalResourceListParcel medicalResourceListParcel) {
2222                                 returnResult(
2223                                         executor,
2224                                         medicalResourceListParcel.getMedicalResources(),
2225                                         callback);
2226                             }
2227 
2228                             @Override
2229                             public void onError(HealthConnectExceptionParcel exception) {
2230                                 returnError(executor, exception, callback);
2231                             }
2232                         });
2233             } else {
2234                 mService.upsertMedicalResources(
2235                         mContext.getAttributionSource(),
2236                         requests,
2237                         new IMedicalResourcesResponseCallback.Stub() {
2238                             @Override
2239                             public void onResult(List<MedicalResource> medicalResources) {
2240                                 returnResult(executor, medicalResources, callback);
2241                             }
2242 
2243                             @Override
2244                             public void onError(HealthConnectExceptionParcel exception) {
2245                                 returnError(executor, exception, callback);
2246                             }
2247                         });
2248             }
2249         } catch (RemoteException e) {
2250             throw e.rethrowFromSystemServer();
2251         }
2252     }
2253 
2254     /**
2255      * Reads {@link MedicalResource}s based on a list of {@link MedicalResourceId}s.
2256      *
2257      * <p>The number and order of medical resources returned by this API is not guaranteed. The
2258      * number will depend on the factors below:
2259      *
2260      * <ul>
2261      *   <li>If an empty list of {@code ids} is provided, an empty list will be returned.
2262      *   <li>If the size of {@code ids} is more than 5000, the API will throw an {@link
2263      *       IllegalArgumentException}.
2264      *   <li>If any ID in {@code ids} is invalid, the API will throw an {@link
2265      *       IllegalArgumentException}.
2266      *   <li>If any ID in {@code ids} does not exist, no medical resource will be returned for that
2267      *       ID.
2268      *   <li>Callers will only get medical resources they are permitted to get. See below.
2269      * </ul>
2270      *
2271      * Being permitted to read medical resources is dependent on the following logic, in priority
2272      * order, earlier statements take precedence.
2273      *
2274      * <ol>
2275      *   <li>A caller with the system permission can get any medical resources in the foreground or
2276      *       background.
2277      *   <li>A caller without any read or write permissions for health data will not be able to get
2278      *       any medical resources and receive an exception with code {@link
2279      *       HealthConnectException#ERROR_SECURITY} via {@code callback.onError()}, even for medical
2280      *       resources the caller has created.
2281      *   <li>Callers can get medical resources they have created, whether this method is called in
2282      *       the foreground or background. Note this only applies if the caller has at least one
2283      *       read or write permission for health data.
2284      *   <li>For any given medical resource, a caller can get that medical resource in the
2285      *       foreground if the caller has the corresponding read permission, or in the background if
2286      *       it also has {@link
2287      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND}.
2288      *   <li>In all other cases the caller is not permitted to get the given medical resource and it
2289      *       will not be returned.
2290      * </ol>
2291      *
2292      * <p>Each returned {@link MedicalResource} has passed the Health Connect FHIR validation checks
2293      * at write time, but is not guaranteed to meet all requirements of the <a
2294      * href="https://hl7.org/fhir/resourcelist.html">Fast Healthcare Interoperability Resources
2295      * (FHIR) spec</a>. If required, clients should perform their own checks on the data.
2296      *
2297      * @param ids Identifiers on which to perform read operation.
2298      * @param executor Executor on which to invoke the callback.
2299      * @param callback Callback to receive result of performing this operation.
2300      * @throws IllegalArgumentException if the size of {@code ids} is more than 5000 or if any id is
2301      *     invalid.
2302      */
2303     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2304     public void readMedicalResources(
2305             @NonNull List<MedicalResourceId> ids,
2306             @NonNull Executor executor,
2307             @NonNull OutcomeReceiver<List<MedicalResource>, HealthConnectException> callback) {
2308         Objects.requireNonNull(ids);
2309         Objects.requireNonNull(executor);
2310         Objects.requireNonNull(callback);
2311 
2312         if (ids.isEmpty()) {
2313             returnResult(executor, List.of(), callback);
2314             return;
2315         }
2316 
2317         if (ids.size() > MAXIMUM_PAGE_SIZE) {
2318             throw new IllegalArgumentException(
2319                     "The number of requested IDs must be <= " + MAXIMUM_PAGE_SIZE);
2320         }
2321 
2322         try {
2323             mService.readMedicalResourcesByIds(
2324                     mContext.getAttributionSource(),
2325                     ids,
2326                     new IReadMedicalResourcesResponseCallback.Stub() {
2327                         @Override
2328                         public void onResult(ReadMedicalResourcesResponse response) {
2329                             returnResult(executor, response.getMedicalResources(), callback);
2330                         }
2331 
2332                         @Override
2333                         public void onError(HealthConnectExceptionParcel exception) {
2334                             returnError(executor, exception, callback);
2335                         }
2336                     });
2337         } catch (RemoteException e) {
2338             throw e.rethrowFromSystemServer();
2339         }
2340     }
2341 
2342     /**
2343      * Reads {@link MedicalResource}s based on {@link ReadMedicalResourcesInitialRequest} or {@link
2344      * ReadMedicalResourcesPageRequest}.
2345      *
2346      * <p>Being permitted to read medical resources is dependent on the following logic, in priority
2347      * order, earlier statements take precedence.
2348      *
2349      * <ol>
2350      *   <li>A caller with the system permission can get any medical resources in the foreground or
2351      *       background.
2352      *   <li>A caller without any read or write permissions for health data will not be able to get
2353      *       any medical resources and receive an exception with code {@link
2354      *       HealthConnectException#ERROR_SECURITY} via {@code callback.onError()}, even for medical
2355      *       resources the caller has created.
2356      *   <li>Callers can get medical resources they have created, whether this method is called in
2357      *       the foreground or background. Note this only applies if the caller has at least one
2358      *       read or write permission for health data.
2359      *   <li>For any given medical resource, a caller can get that medical resource in the
2360      *       foreground if the caller has the corresponding read permission, or in the background if
2361      *       it also has {@link
2362      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND}.
2363      *   <li>In all other cases the caller is not permitted to get the given medical resource and it
2364      *       will not be returned.
2365      * </ol>
2366      *
2367      * @param request The read request {@link ReadMedicalResourcesInitialRequest} or {@link
2368      *     ReadMedicalResourcesPageRequest}.
2369      * @param executor Executor on which to invoke the callback.
2370      * @param callback Callback to receive result of performing this operation.
2371      * @throws IllegalArgumentException if {@code request} has set page size to be less than 1 or
2372      *     more than 5000; or if contains unsupported medical resource type or invalid {@link
2373      *     MedicalDataSource} IDs when using {@link ReadMedicalResourcesInitialRequest}.
2374      */
2375     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2376     public void readMedicalResources(
2377             @NonNull ReadMedicalResourcesRequest request,
2378             @NonNull Executor executor,
2379             @NonNull
2380                     OutcomeReceiver<ReadMedicalResourcesResponse, HealthConnectException>
2381                             callback) {
2382         Objects.requireNonNull(request);
2383         Objects.requireNonNull(executor);
2384         Objects.requireNonNull(callback);
2385 
2386         try {
2387             mService.readMedicalResourcesByRequest(
2388                     mContext.getAttributionSource(),
2389                     request.toParcel(),
2390                     new IReadMedicalResourcesResponseCallback.Stub() {
2391                         @Override
2392                         public void onResult(ReadMedicalResourcesResponse response) {
2393                             returnResult(executor, response, callback);
2394                         }
2395 
2396                         @Override
2397                         public void onError(HealthConnectExceptionParcel exception) {
2398                             returnError(executor, exception, callback);
2399                         }
2400                     });
2401         } catch (RemoteException e) {
2402             throw e.rethrowFromSystemServer();
2403         }
2404     }
2405 
2406     /**
2407      * Deletes {@link MedicalResource}s based on given filters in {@link
2408      * DeleteMedicalResourcesRequest}.
2409      *
2410      * <p>Regarding permissions:
2411      *
2412      * <ul>
2413      *   <li>Only apps with the system permission can delete data written by apps other than
2414      *       themselves.
2415      *   <li>Deletes are permitted in the foreground or background.
2416      * </ul>
2417      *
2418      * @param request The delete request.
2419      * @param executor Executor on which to invoke the callback.
2420      * @param callback Callback to receive result of performing this operation.
2421      * @throws IllegalArgumentException if {@code request} contains unsupported medical resource
2422      *     types or invalid {@link MedicalDataSource} IDs.
2423      */
2424     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2425     // Suppress missing because API flagged out. "RequiresPermission" is also needed because
2426     // @RequiresPermission generates javadoc for the flagged out permission.
2427     // TODO: b/355156275 - remove suppression once API not flagged out.
2428     @SuppressWarnings({"MissingPermission", "RequiresPermission"})
2429     @RequiresPermission(anyOf = {WRITE_MEDICAL_DATA, MANAGE_HEALTH_DATA_PERMISSION})
2430     public void deleteMedicalResources(
2431             @NonNull DeleteMedicalResourcesRequest request,
2432             @NonNull Executor executor,
2433             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
2434         Objects.requireNonNull(request);
2435         Objects.requireNonNull(executor);
2436         Objects.requireNonNull(callback);
2437 
2438         try {
2439             mService.deleteMedicalResourcesByRequest(
2440                     mContext.getAttributionSource(),
2441                     request,
2442                     new IEmptyResponseCallback.Stub() {
2443                         @Override
2444                         public void onResult() {
2445                             returnResult(executor, null, callback);
2446                         }
2447 
2448                         @Override
2449                         public void onError(HealthConnectExceptionParcel exception) {
2450                             returnError(executor, exception, callback);
2451                         }
2452                     });
2453         } catch (RemoteException e) {
2454             throw e.rethrowFromSystemServer();
2455         }
2456     }
2457 
2458     /**
2459      * Deletes a list of {@link MedicalResource}s by the provided list of {@link
2460      * MedicalResourceId}s.
2461      *
2462      * <ul>
2463      *   <li>If any ID in {@code ids} is invalid, the API will throw an {@link
2464      *       IllegalArgumentException}, and nothing will be deleted.
2465      *   <li>If any ID in {@code ids} does not exist, that ID will be ignored, while deletion on
2466      *       other IDs will be performed.
2467      * </ul>
2468      *
2469      * <p>Regarding permissions:
2470      *
2471      * <ul>
2472      *   <li>Only apps with the system permission can delete data written by apps other than
2473      *       themselves.
2474      *   <li>Deletes are permitted in the foreground or background.
2475      * </ul>
2476      *
2477      * @param ids The ids to delete.
2478      * @param executor Executor on which to invoke the callback.
2479      * @param callback Callback to receive result of performing this operation.
2480      */
2481     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2482     // Suppress missing because API flagged out. "RequiresPermission" is also needed because
2483     // @RequiresPermission generates javadoc for the flagged out permission.
2484     // TODO: b/355156275 - remove suppression once API not flagged out.
2485     @SuppressWarnings({"MissingPermission", "RequiresPermission"})
2486     @RequiresPermission(anyOf = {WRITE_MEDICAL_DATA, MANAGE_HEALTH_DATA_PERMISSION})
2487     public void deleteMedicalResources(
2488             @NonNull List<MedicalResourceId> ids,
2489             @NonNull Executor executor,
2490             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
2491         Objects.requireNonNull(ids);
2492         Objects.requireNonNull(executor);
2493         Objects.requireNonNull(callback);
2494 
2495         if (ids.isEmpty()) {
2496             returnResult(executor, null, callback);
2497             return;
2498         }
2499 
2500         try {
2501             mService.deleteMedicalResourcesByIds(
2502                     mContext.getAttributionSource(),
2503                     ids,
2504                     new IEmptyResponseCallback.Stub() {
2505                         @Override
2506                         public void onResult() {
2507                             returnResult(executor, null, callback);
2508                         }
2509 
2510                         @Override
2511                         public void onError(HealthConnectExceptionParcel exception) {
2512                             returnError(executor, exception, callback);
2513                         }
2514                     });
2515         } catch (RemoteException e) {
2516             throw e.rethrowFromSystemServer();
2517         }
2518     }
2519 
2520     /**
2521      * Retrieves information about all medical resource types and returns a list of {@link
2522      * MedicalResourceTypeInfo}.
2523      *
2524      * @param executor Executor on which to invoke the callback.
2525      * @param callback Callback to receive result of performing this operation.
2526      * @hide
2527      */
2528     @SystemApi
2529     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2530     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
2531     public void queryAllMedicalResourceTypeInfos(
2532             @NonNull @CallbackExecutor Executor executor,
2533             @NonNull
2534                     OutcomeReceiver<List<MedicalResourceTypeInfo>, HealthConnectException>
2535                             callback) {
2536         Objects.requireNonNull(executor);
2537         Objects.requireNonNull(callback);
2538 
2539         try {
2540             mService.queryAllMedicalResourceTypeInfos(
2541                     new IMedicalResourceTypeInfosCallback.Stub() {
2542                         @Override
2543                         public void onResult(List<MedicalResourceTypeInfo> response) {
2544                             Binder.clearCallingIdentity();
2545                             executor.execute(() -> callback.onResult(response));
2546                         }
2547 
2548                         @Override
2549                         public void onError(HealthConnectExceptionParcel exception) {
2550                             returnError(executor, exception, callback);
2551                         }
2552                     });
2553         } catch (RemoteException e) {
2554             throw e.rethrowFromSystemServer();
2555         }
2556     }
2557 
2558     /**
2559      * Creates a {@link MedicalDataSource} in HealthConnect based on the {@link
2560      * CreateMedicalDataSourceRequest} request values.
2561      *
2562      * <p>Medical data is represented using the <a href="https://hl7.org/fhir/">Fast Healthcare
2563      * Interoperability Resources (FHIR)</a> standard.
2564      *
2565      * <p>A {@link MedicalDataSource} needs to be created before any {@link MedicalResource}s for
2566      * that source can be inserted. Separate {@link MedicalDataSource}s should be created for
2567      * medical records coming from different sources (e.g. different FHIR endpoints, different
2568      * healthcare systems), unless the data has been reconciled and all records have a unique
2569      * combination of resource type and resource id.
2570      *
2571      * <p>The {@link CreateMedicalDataSourceRequest.Builder#setDisplayName display name} must be
2572      * unique per app, and {@link CreateMedicalDataSourceRequest.Builder#setFhirVersion} FHIR
2573      * version} must be a version supported by Health Connect, as documented on the {@link
2574      * FhirVersion}. See {@link CreateMedicalDataSourceRequest.Builder#setFhirBaseUri} for more
2575      * details on the FHIR base URI.
2576      *
2577      * @param request Creation request.
2578      * @param executor Executor on which to invoke the callback.
2579      * @param callback Callback to receive result of performing this operation.
2580      * @throws IllegalArgumentException if {@code request} contains a FHIR base URI or display name
2581      *     exceeding the character limits, or an unsupported FHIR version.
2582      */
2583     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2584     // Suppress missing because API flagged out.
2585     // TODO: b/355156275 - remove suppression once API not flagged out.
2586     @SuppressWarnings({"MissingPermission"})
2587     @RequiresPermission(WRITE_MEDICAL_DATA)
2588     public void createMedicalDataSource(
2589             @NonNull CreateMedicalDataSourceRequest request,
2590             @NonNull Executor executor,
2591             @NonNull OutcomeReceiver<MedicalDataSource, HealthConnectException> callback) {
2592         Objects.requireNonNull(request);
2593         Objects.requireNonNull(executor);
2594         Objects.requireNonNull(callback);
2595 
2596         try {
2597             mService.createMedicalDataSource(
2598                     mContext.getAttributionSource(),
2599                     request,
2600                     new IMedicalDataSourceResponseCallback.Stub() {
2601                         @Override
2602                         public void onResult(MedicalDataSource dataSource) {
2603                             returnResult(executor, dataSource, callback);
2604                         }
2605 
2606                         @Override
2607                         public void onError(HealthConnectExceptionParcel exception) {
2608                             returnError(executor, exception, callback);
2609                         }
2610                     });
2611         } catch (RemoteException e) {
2612             throw e.rethrowFromSystemServer();
2613         }
2614     }
2615 
2616     /**
2617      * Returns {@link MedicalDataSource}s for the provided list of IDs.
2618      *
2619      * <p>The number and order of medical data sources returned by this API is not guaranteed. The
2620      * number will depend on the factors below:
2621      *
2622      * <ul>
2623      *   <li>If an empty list of {@code ids} is provided, an empty list will be returned.
2624      *   <li>If any ID in {@code ids} is invalid, the caller will receive an exception with code
2625      *       {@link HealthConnectException#ERROR_INVALID_ARGUMENT} via {@code callback.onError()}.
2626      *   <li>If any ID in {@code ids} does not exist, no data source will be returned for that ID.
2627      *   <li>Callers will only get data sources they are permitted to get. See below.
2628      * </ul>
2629      *
2630      * <p>There is no specific read permission for getting data sources. Instead, permission to read
2631      * data sources is based on whether the caller has permission to read the data currently linked
2632      * to that data source. Being permitted to get data sources is dependent on the following logic,
2633      * in priority order, earlier statements take precedence.
2634      *
2635      * <ol>
2636      *   <li>A caller with the system permission can get any data source in the foreground or
2637      *       background.
2638      *   <li>A caller without any read or write permissions for health data will not be able to get
2639      *       any medical data sources and receive an exception with code {@link
2640      *       HealthConnectException#ERROR_SECURITY} via {@code callback.onError()}, even for data
2641      *       sources the caller has created.
2642      *   <li>Callers can get data sources they have created, whether this method is called in the
2643      *       foreground or background. Note this only applies if the caller has at least one read or
2644      *       write permission for health data.
2645      *   <li>For any given data source, a caller can get that data source in the foreground if the
2646      *       caller has permission to read any of the data linked to that data source. For clarity,
2647      *       the does not allow it to get an empty data source.
2648      *   <li>For any given data source, a caller can get that data source in the background if it
2649      *       has both permission to read any of the data linked to that data source, and {@link
2650      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND}.
2651      *   <li>In all other cases the caller is not permitted to get the given data source and it will
2652      *       not be returned.
2653      * </ol>
2654      *
2655      * @param ids Identifiers for data sources to get.
2656      * @param executor Executor on which to invoke the callback.
2657      * @param callback Callback to receive result of performing this operation.
2658      * @throws IllegalArgumentException if the size of {@code ids} is more than 5000.
2659      */
2660     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2661     public void getMedicalDataSources(
2662             @NonNull List<String> ids,
2663             @NonNull Executor executor,
2664             @NonNull OutcomeReceiver<List<MedicalDataSource>, HealthConnectException> callback) {
2665         Objects.requireNonNull(ids);
2666         Objects.requireNonNull(executor);
2667         Objects.requireNonNull(callback);
2668 
2669         if (ids.isEmpty()) {
2670             returnResult(executor, List.of(), callback);
2671             return;
2672         }
2673 
2674         if (ids.size() > MAXIMUM_PAGE_SIZE) {
2675             throw new IllegalArgumentException(
2676                     "The number of requested IDs must be <= " + MAXIMUM_PAGE_SIZE);
2677         }
2678 
2679         try {
2680             mService.getMedicalDataSourcesByIds(
2681                     mContext.getAttributionSource(),
2682                     ids,
2683                     new IMedicalDataSourcesResponseCallback.Stub() {
2684                         @Override
2685                         public void onResult(List<MedicalDataSource> result) {
2686                             returnResult(executor, result, callback);
2687                         }
2688 
2689                         @Override
2690                         public void onError(HealthConnectExceptionParcel exception) {
2691                             returnError(executor, exception, callback);
2692                         }
2693                     });
2694         } catch (RemoteException e) {
2695             throw e.rethrowFromSystemServer();
2696         }
2697     }
2698 
2699     /**
2700      * Returns the requested {@link MedicalDataSource}s.
2701      *
2702      * <p>Number of data sources returned by this API will depend based on below factors:
2703      *
2704      * <ul>
2705      *   <li>If an empty {@link GetMedicalDataSourcesRequest#getPackageNames() list of package
2706      *       names} is passed, all permitted data sources from all apps will be returned. See below.
2707      *   <li>If any package name in the {@link GetMedicalDataSourcesRequest#getPackageNames() list
2708      *       of package names} is invalid, the API will throw an {@link IllegalArgumentException}.
2709      *   <li>If a non-empty {@link GetMedicalDataSourcesRequest#getPackageNames() list of package
2710      *       names} is specified in the request, then only the permitted data sources created by
2711      *       those packages will be returned. See below.
2712      * </ul>
2713      *
2714      * <p>There is no specific read permission for getting data sources. Instead permission to read
2715      * data sources is based on whether the caller has permission to read the data currently linked
2716      * to that data source. Being permitted to get data sources is dependent on the following logic,
2717      * in priority order, earlier statements take precedence.
2718      *
2719      * <ol>
2720      *   <li>A caller with the system permission can get any data source in the foreground or
2721      *       background.
2722      *   <li>A caller without any read or write permissions for health data will not be able to get
2723      *       any medical data sources and receive an exception with code {@link
2724      *       HealthConnectException#ERROR_SECURITY} via {@code callback.onError()}, even for data
2725      *       sources the caller has created.
2726      *   <li>Callers can get data sources they have created, whether this method is called in the
2727      *       foreground or background. Note this only applies if the caller has at least one read or
2728      *       write permission for health data.
2729      *   <li>For any given data source, a caller can get that data source in the foreground if the
2730      *       caller has permission to read any of the data linked to that data source. For clarity,
2731      *       the does not allow it to get an empty data source.
2732      *   <li>For any given data source, a caller can get that data source in the background if it
2733      *       has both permission to read any of the data linked to that data source, and {@link
2734      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND}.
2735      *   <li>In all other cases the caller is not permitted to get the given data source and it will
2736      *       not be returned.
2737      * </ol>
2738      *
2739      * @param request the request for which data sources to return.
2740      * @param executor Executor on which to invoke the callback.
2741      * @param callback Callback to receive result of performing this operation.
2742      * @throws IllegalArgumentException if {@code request} contains invalid package names.
2743      */
2744     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2745     public void getMedicalDataSources(
2746             @NonNull GetMedicalDataSourcesRequest request,
2747             @NonNull Executor executor,
2748             @NonNull OutcomeReceiver<List<MedicalDataSource>, HealthConnectException> callback) {
2749         Objects.requireNonNull(request);
2750         Objects.requireNonNull(executor);
2751         Objects.requireNonNull(callback);
2752 
2753         try {
2754             mService.getMedicalDataSourcesByRequest(
2755                     mContext.getAttributionSource(),
2756                     request,
2757                     new IMedicalDataSourcesResponseCallback.Stub() {
2758                         @Override
2759                         public void onResult(List<MedicalDataSource> result) {
2760                             returnResult(executor, result, callback);
2761                         }
2762 
2763                         @Override
2764                         public void onError(HealthConnectExceptionParcel exception) {
2765                             returnError(executor, exception, callback);
2766                         }
2767                     });
2768         } catch (RemoteException e) {
2769             throw e.rethrowFromSystemServer();
2770         }
2771     }
2772 
2773     /**
2774      * Deletes a {@link MedicalDataSource} and all data linked to it.
2775      *
2776      * <p>If the provided data source {@code id} is either invalid, or does not exist, or owned by
2777      * another apps, the caller will receive an exception with code {@link
2778      * HealthConnectException#ERROR_INVALID_ARGUMENT} via {@code callback.onError()}.
2779      *
2780      * <p>Regarding permissions:
2781      *
2782      * <ul>
2783      *   <li>Only apps with the system permission can delete data written by apps other than
2784      *       themselves.
2785      *   <li>Deletes are permitted in the foreground or background.
2786      * </ul>
2787      *
2788      * @param id The id of the data source to delete.
2789      * @param executor Executor on which to invoke the callback.
2790      * @param callback Callback to receive result of performing this operation.
2791      */
2792     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2793     // Suppress missing because API flagged out. "RequiresPermission" is also needed because
2794     // @RequiresPermission generates javadoc for the flagged out permission.
2795     // TODO: b/355156275 - remove suppression once API not flagged out.
2796     @SuppressWarnings({"MissingPermission", "RequiresPermission"})
2797     @RequiresPermission(anyOf = {WRITE_MEDICAL_DATA, MANAGE_HEALTH_DATA_PERMISSION})
2798     public void deleteMedicalDataSourceWithData(
2799             @NonNull String id,
2800             @NonNull Executor executor,
2801             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
2802 
2803         Objects.requireNonNull(id);
2804         Objects.requireNonNull(executor);
2805         Objects.requireNonNull(callback);
2806 
2807         try {
2808             mService.deleteMedicalDataSourceWithData(
2809                     mContext.getAttributionSource(),
2810                     id,
2811                     new IEmptyResponseCallback.Stub() {
2812                         @Override
2813                         public void onResult() {
2814                             returnResult(executor, null, callback);
2815                         }
2816 
2817                         @Override
2818                         public void onError(HealthConnectExceptionParcel exception) {
2819                             returnError(executor, exception, callback);
2820                         }
2821                     });
2822         } catch (RemoteException e) {
2823             throw e.rethrowFromSystemServer();
2824         }
2825     }
2826 
2827     /**
2828      * Returns a list of changes to be backed up based on a given change token.
2829      *
2830      * <p>The change token returned by the previous call should be passed in to resume the upload.
2831      * The caller should always store the previous change token returned in {@link
2832      * GetChangesForBackupResponse} and pass it into the next {@code getChangesForBackup} call.
2833      *
2834      * <p>A null change token means the caller is starting a fresh backup which starts from backing
2835      * up all data of the Health Connect data tables. After backing up all data tables, the API
2836      * switches to incremental backups using change logs.
2837      *
2838      * <p>If the change token is not found, it means that HealthConnect can no longer resume the
2839      * backup, and will respond with a {@link HealthConnectException#ERROR_INVALID_ARGUMENT}. In
2840      * this case the caller should restart the backup by calling {@code getChangesForBackup} with a
2841      * null change token.
2842      *
2843      * <p>If no changes are returned by the API, this means that the client has synced all changes
2844      * as of now. The caller should keep the change token returned in {@link
2845      * GetChangesForBackupResponse} to later resume the backup from this point.
2846      *
2847      * <p>The number of changes returned by this API is not guaranteed. The number will depend on
2848      * the factors below:
2849      *
2850      * <ul>
2851      *   <li>At the data table backup stage, up to 1000 changes will be returned per call. The last
2852      *       page may contain fewer than 1000 changes.
2853      *   <li>During incremental backups, data changes contained in up to 1000 change logs will be
2854      *       returned.
2855      * </ul>
2856      *
2857      * @param changeToken The token to resume the backup from.
2858      * @param executor Executor on which to invoke the callback.
2859      * @param callback Callback to receive the result of performing this operation.
2860      * @hide
2861      */
2862     @SystemApi
2863     @SuppressWarnings("NullAway") // TODO: b/178748627 - fix this suppression.
2864     @FlaggedApi(FLAG_CLOUD_BACKUP_AND_RESTORE)
2865     @RequiresPermission(BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS)
2866     public void getChangesForBackup(
2867             @Nullable String changeToken,
2868             @NonNull Executor executor,
2869             @NonNull
2870                     OutcomeReceiver<GetChangesForBackupResponse, HealthConnectException> callback) {
2871         Objects.requireNonNull(executor);
2872         Objects.requireNonNull(callback);
2873         try {
2874             mService.getChangesForBackup(
2875                     changeToken,
2876                     new IGetChangesForBackupResponseCallback.Stub() {
2877                         @Override
2878                         public void onResult(GetChangesForBackupResponse response) {
2879                             returnResult(executor, response, callback);
2880                         }
2881 
2882                         @Override
2883                         public void onError(HealthConnectExceptionParcel exception) {
2884                             returnError(executor, exception, callback);
2885                         }
2886                     });
2887         } catch (RemoteException e) {
2888             throw e.rethrowFromSystemServer();
2889         }
2890     }
2891 
2892     /**
2893      * Returns all metadata to be backed up bundled as a single byte array.
2894      *
2895      * <p>This method returns a {@link BackupMetadata} object containing the user's Health Connect
2896      * metadata that should be backed up. The returned response is the latest snapshot of the Health
2897      * Connect metadata at the time of the call.
2898      *
2899      * <p>The caller should call this method at the end of each backup cycle to capture the latest
2900      * metadata, after calling {@link #getChangesForBackup} to fetch record changes. A backup cycle
2901      * is the process by which user data and metadata on a device are periodically saved to the
2902      * cloud. This ensures that the most up-to-date metadata snapshot are preserved in the backup.
2903      *
2904      * @param executor Executor on which to invoke the callback.
2905      * @param callback Callback to receive the result of performing this operation.
2906      * @hide
2907      */
2908     @SystemApi
2909     @FlaggedApi(FLAG_CLOUD_BACKUP_AND_RESTORE)
2910     @RequiresPermission(BACKUP_HEALTH_CONNECT_DATA_AND_SETTINGS)
2911     public void getLatestMetadataForBackup(
2912             @NonNull Executor executor,
2913             @NonNull
2914                     OutcomeReceiver<GetLatestMetadataForBackupResponse, HealthConnectException>
2915                             callback) {
2916         Objects.requireNonNull(executor);
2917         Objects.requireNonNull(callback);
2918         try {
2919             mService.getLatestMetadataForBackup(
2920                     new IGetLatestMetadataForBackupResponseCallback.Stub() {
2921                         @Override
2922                         public void onResult(GetLatestMetadataForBackupResponse response) {
2923                             returnResult(executor, response, callback);
2924                         }
2925 
2926                         @Override
2927                         public void onError(HealthConnectExceptionParcel exception) {
2928                             returnError(executor, exception, callback);
2929                         }
2930                     });
2931         } catch (RemoteException e) {
2932             throw e.rethrowFromSystemServer();
2933         }
2934     }
2935 
2936     /**
2937      * Restores the Health Connect metadata from a latest backup.
2938      *
2939      * <p>This method restores metadata previously backed up using {@link
2940      * #getLatestMetadataForBackup}. The provided {@code backupMetadata} object should contain the
2941      * metadata to be restored.
2942      *
2943      * <p>The callback will receive a {@link HealthConnectException} with {@code
2944      * HealthConnectException#ERROR_INVALID_ARGUMENT} if the provided {@code backupMetadata} can not
2945      * be parsed into valid metadata. The caller can retry with the same metadata once {@link
2946      * #canRestore} returns true. Other exceptions should not be retried.
2947      *
2948      * <p>The caller should call this method before calling {@link #restoreChanges}.
2949      *
2950      * @param backupMetadata The metadata to be restored.
2951      * @param executor Executor on which to invoke the callback.
2952      * @param callback Callback to receive the result of performing this operation.
2953      * @hide
2954      */
2955     @SystemApi
2956     @FlaggedApi(FLAG_CLOUD_BACKUP_AND_RESTORE)
2957     @RequiresPermission(RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS)
2958     public void restoreLatestMetadata(
2959             @NonNull BackupMetadata backupMetadata,
2960             @NonNull Executor executor,
2961             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
2962         Objects.requireNonNull(backupMetadata);
2963         Objects.requireNonNull(executor);
2964         Objects.requireNonNull(callback);
2965         try {
2966             mService.restoreLatestMetadata(
2967                     backupMetadata,
2968                     new IEmptyResponseCallback.Stub() {
2969                         @Override
2970                         public void onResult() {
2971                             returnResult(executor, null, callback);
2972                         }
2973 
2974                         @Override
2975                         public void onError(HealthConnectExceptionParcel exception) {
2976                             returnError(executor, exception, callback);
2977                         }
2978                     });
2979         } catch (RemoteException e) {
2980             throw e.rethrowFromSystemServer();
2981         }
2982     }
2983 
2984     /**
2985      * Checks if the data can be restored based on the version which the data is serialized with.
2986      *
2987      * <p>If the data version is not yet supported by the Health Connect module, because the backup
2988      * was done on a newer version of Health Connect, this method will return false.
2989      *
2990      * <p>The caller should call this API before calling {@link #restoreLatestMetadata} or {@link
2991      * #restoreChanges}. If this method returns true, then the caller can continue with sending
2992      * either metadata or data to Health Connect for restore.
2993      *
2994      * @param dataVersion The version of the data to be restored.
2995      * @param executor Executor on which to invoke the callback.
2996      * @param callback Callback to receive the result of performing this operation.
2997      * @hide
2998      */
2999     @SystemApi
3000     @FlaggedApi(FLAG_CLOUD_BACKUP_AND_RESTORE)
3001     @RequiresPermission(RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS)
3002     public void canRestore(
3003             int dataVersion,
3004             @NonNull Executor executor,
3005             @NonNull OutcomeReceiver<Boolean, HealthConnectException> callback) {
3006         Objects.requireNonNull(executor);
3007         Objects.requireNonNull(callback);
3008         try {
3009             mService.canRestore(
3010                     dataVersion,
3011                     new ICanRestoreResponseCallback.Stub() {
3012                         @Override
3013                         public void onResult(boolean canRestore) {
3014                             returnResult(executor, canRestore, callback);
3015                         }
3016 
3017                         @Override
3018                         public void onError(HealthConnectExceptionParcel exception) {
3019                             returnError(executor, exception, callback);
3020                         }
3021                     });
3022         } catch (RemoteException e) {
3023             throw e.rethrowFromSystemServer();
3024         }
3025     }
3026 
3027     /**
3028      * Restores changes to Health Connect from a backup.
3029      *
3030      * <p>This method applies a list of changes previously backed up. Each {@link RestoreChange}
3031      * object represents an individual change, corresponding to the insertion or update of a record.
3032      * If a change is restored multiple times, the consecutive restores will be ignored by the
3033      * Health Connect module.
3034      *
3035      * <p>The caller should call {@link #restoreLatestMetadata} with the latest metadata it received
3036      * before calling this method. The caller should also send the changes in the order they were
3037      * received.
3038      *
3039      * <p>If any {@link RestoreChange} is not yet supported by the Health Connect module or can not
3040      * be parsed into valid record, {@link HealthConnectException#ERROR_INVALID_ARGUMENT} will be
3041      * thrown. The caller can retry with the same changes once {@link #canRestore} returns true.
3042      * Other exceptions should not be retried.
3043      *
3044      * @param restoreChanges The changes to be restored back to Health Connect.
3045      * @param executor Executor on which to invoke the callback.
3046      * @param callback Callback to receive the result of performing this operation.
3047      * @hide
3048      */
3049     @SystemApi
3050     @FlaggedApi(FLAG_CLOUD_BACKUP_AND_RESTORE)
3051     @RequiresPermission(RESTORE_HEALTH_CONNECT_DATA_AND_SETTINGS)
3052     public void restoreChanges(
3053             @NonNull List<RestoreChange> restoreChanges,
3054             @NonNull Executor executor,
3055             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
3056         Objects.requireNonNull(restoreChanges);
3057         Objects.requireNonNull(executor);
3058         Objects.requireNonNull(callback);
3059         try {
3060             mService.restoreChanges(
3061                     restoreChanges,
3062                     new IEmptyResponseCallback.Stub() {
3063                         @Override
3064                         public void onResult() {
3065                             returnResult(executor, null, callback);
3066                         }
3067 
3068                         @Override
3069                         public void onError(HealthConnectExceptionParcel exception) {
3070                             returnError(executor, exception, callback);
3071                         }
3072                     });
3073         } catch (RemoteException e) {
3074             throw e.rethrowFromSystemServer();
3075         }
3076     }
3077 }
3078