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.health.connect.Constants.DEFAULT_LONG;
20 import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE;
21 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
22 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
23 
24 import static com.android.healthfitness.flags.Flags.FLAG_EXPORT_IMPORT;
25 import static com.android.healthfitness.flags.Flags.FLAG_PERSONAL_HEALTH_RECORD;
26 
27 import android.Manifest;
28 import android.annotation.CallbackExecutor;
29 import android.annotation.FlaggedApi;
30 import android.annotation.IntDef;
31 import android.annotation.IntRange;
32 import android.annotation.NonNull;
33 import android.annotation.Nullable;
34 import android.annotation.RequiresPermission;
35 import android.annotation.SdkConstant;
36 import android.annotation.SystemApi;
37 import android.annotation.SystemService;
38 import android.annotation.TestApi;
39 import android.annotation.UserHandleAware;
40 import android.annotation.WorkerThread;
41 import android.content.Context;
42 import android.content.pm.PackageInfo;
43 import android.content.pm.PackageManager;
44 import android.content.pm.PermissionGroupInfo;
45 import android.content.pm.PermissionInfo;
46 import android.health.connect.accesslog.AccessLog;
47 import android.health.connect.accesslog.AccessLogsResponseParcel;
48 import android.health.connect.aidl.ActivityDatesRequestParcel;
49 import android.health.connect.aidl.ActivityDatesResponseParcel;
50 import android.health.connect.aidl.AggregateDataRequestParcel;
51 import android.health.connect.aidl.AggregateDataResponseParcel;
52 import android.health.connect.aidl.ApplicationInfoResponseParcel;
53 import android.health.connect.aidl.DeleteUsingFiltersRequestParcel;
54 import android.health.connect.aidl.GetPriorityResponseParcel;
55 import android.health.connect.aidl.HealthConnectExceptionParcel;
56 import android.health.connect.aidl.IAccessLogsResponseCallback;
57 import android.health.connect.aidl.IActivityDatesResponseCallback;
58 import android.health.connect.aidl.IAggregateRecordsResponseCallback;
59 import android.health.connect.aidl.IApplicationInfoResponseCallback;
60 import android.health.connect.aidl.IChangeLogsResponseCallback;
61 import android.health.connect.aidl.IDataStagingFinishedCallback;
62 import android.health.connect.aidl.IEmptyResponseCallback;
63 import android.health.connect.aidl.IGetChangeLogTokenCallback;
64 import android.health.connect.aidl.IGetHealthConnectDataStateCallback;
65 import android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback;
66 import android.health.connect.aidl.IGetPriorityResponseCallback;
67 import android.health.connect.aidl.IHealthConnectService;
68 import android.health.connect.aidl.IInsertRecordsResponseCallback;
69 import android.health.connect.aidl.IMigrationCallback;
70 import android.health.connect.aidl.IReadMedicalResourcesResponseCallback;
71 import android.health.connect.aidl.IReadRecordsResponseCallback;
72 import android.health.connect.aidl.IRecordTypeInfoResponseCallback;
73 import android.health.connect.aidl.InsertRecordsResponseParcel;
74 import android.health.connect.aidl.MedicalIdFiltersParcel;
75 import android.health.connect.aidl.ReadRecordsResponseParcel;
76 import android.health.connect.aidl.RecordIdFiltersParcel;
77 import android.health.connect.aidl.RecordTypeInfoResponseParcel;
78 import android.health.connect.aidl.RecordsParcel;
79 import android.health.connect.aidl.UpdatePriorityRequestParcel;
80 import android.health.connect.changelog.ChangeLogTokenRequest;
81 import android.health.connect.changelog.ChangeLogTokenResponse;
82 import android.health.connect.changelog.ChangeLogsRequest;
83 import android.health.connect.changelog.ChangeLogsResponse;
84 import android.health.connect.datatypes.AggregationType;
85 import android.health.connect.datatypes.DataOrigin;
86 import android.health.connect.datatypes.MedicalDataSource;
87 import android.health.connect.datatypes.MedicalResource;
88 import android.health.connect.datatypes.Record;
89 import android.health.connect.exportimport.ExportImportDocumentProvider;
90 import android.health.connect.exportimport.IImportStatusCallback;
91 import android.health.connect.exportimport.IQueryDocumentProvidersCallback;
92 import android.health.connect.exportimport.IScheduledExportStatusCallback;
93 import android.health.connect.exportimport.ImportStatus;
94 import android.health.connect.exportimport.ScheduledExportSettings;
95 import android.health.connect.exportimport.ScheduledExportStatus;
96 import android.health.connect.internal.datatypes.RecordInternal;
97 import android.health.connect.internal.datatypes.utils.InternalExternalRecordConverter;
98 import android.health.connect.migration.HealthConnectMigrationUiState;
99 import android.health.connect.migration.MigrationEntity;
100 import android.health.connect.migration.MigrationEntityParcel;
101 import android.health.connect.migration.MigrationException;
102 import android.health.connect.restore.StageRemoteDataException;
103 import android.health.connect.restore.StageRemoteDataRequest;
104 import android.net.Uri;
105 import android.os.Binder;
106 import android.os.OutcomeReceiver;
107 import android.os.ParcelFileDescriptor;
108 import android.os.RemoteException;
109 import android.os.UserHandle;
110 import android.util.Log;
111 
112 import java.lang.annotation.Retention;
113 import java.lang.annotation.RetentionPolicy;
114 import java.time.Duration;
115 import java.time.Instant;
116 import java.time.LocalDate;
117 import java.time.Period;
118 import java.time.ZoneOffset;
119 import java.util.ArrayList;
120 import java.util.Collections;
121 import java.util.HashSet;
122 import java.util.List;
123 import java.util.Map;
124 import java.util.Objects;
125 import java.util.Set;
126 import java.util.concurrent.Executor;
127 import java.util.stream.Collectors;
128 
129 /**
130  * This class provides APIs to interact with the centralized HealthConnect storage maintained by the
131  * system.
132  *
133  * <p>HealthConnect is an offline, on-device storage that unifies data from multiple devices and
134  * apps into an ecosystem featuring.
135  *
136  * <ul>
137  *   <li>APIs to insert data of various types into the system.
138  * </ul>
139  *
140  * <p>The basic unit of data in HealthConnect is represented as a {@link Record} object, which is
141  * the base class for all the other data types such as {@link
142  * android.health.connect.datatypes.StepsRecord}.
143  */
144 @SystemService(Context.HEALTHCONNECT_SERVICE)
145 public class HealthConnectManager {
146     /**
147      * Used in conjunction with {@link android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} to
148      * launch UI to show an app’s health permission rationale/data policy.
149      *
150      * <p><b>Note:</b> Used by apps to define an intent filter in conjunction with {@link
151      * android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} that the HC UI can link out to.
152      */
153     // We use intent.category prefix to be compatible with HealthPermissions strings definitions.
154     @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
155     public static final String CATEGORY_HEALTH_PERMISSIONS =
156             "android.intent.category.HEALTH_PERMISSIONS";
157 
158     /**
159      * Activity action: Launch UI to manage (e.g. grant/revoke) health permissions.
160      *
161      * <p>Shows a list of apps which request at least one permission of the Health permission group.
162      *
163      * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the
164      * app requesting the action. Optional: Adding package name extras launches a UI to manager
165      * (e.g. grant/revoke) for this app.
166      */
167     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
168     public static final String ACTION_MANAGE_HEALTH_PERMISSIONS =
169             "android.health.connect.action.MANAGE_HEALTH_PERMISSIONS";
170 
171     /**
172      * Activity action: Launch UI to share the route associated with an exercise session.
173      *
174      * <p>Input: caller must provide `String` extra EXTRA_SESSION_ID
175      *
176      * <p>Result will be delivered via [Activity.onActivityResult] with `ExerciseRoute`
177      * EXTRA_EXERCISE_ROUTE.
178      */
179     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
180     public static final String ACTION_REQUEST_EXERCISE_ROUTE =
181             "android.health.connect.action.REQUEST_EXERCISE_ROUTE";
182 
183     /**
184      * A string ID of a session to be used with {@link #ACTION_REQUEST_EXERCISE_ROUTE}.
185      *
186      * <p>This is used to specify route of which exercise session we want to request.
187      */
188     public static final String EXTRA_SESSION_ID = "android.health.connect.extra.SESSION_ID";
189 
190     /**
191      * An exercise route requested via {@link #ACTION_REQUEST_EXERCISE_ROUTE}.
192      *
193      * <p>This is returned for a successful request to access a route associated with an exercise
194      * session.
195      */
196     public static final String EXTRA_EXERCISE_ROUTE = "android.health.connect.extra.EXERCISE_ROUTE";
197 
198     /**
199      * Activity action: Launch UI to show and manage (e.g. grant/revoke) health permissions.
200      *
201      * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the
202      * app requesting the action must be present. An app can open only its own page.
203      *
204      * <p>Input: caller must provide `String[]` extra [EXTRA_PERMISSIONS]
205      *
206      * <p>Result will be delivered via [Activity.onActivityResult] with `String[]`
207      * [EXTRA_PERMISSIONS] and `int[]` [EXTRA_PERMISSION_GRANT_RESULTS], similar to
208      * [Activity.onRequestPermissionsResult]
209      *
210      * @hide
211      */
212     @SystemApi
213     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
214     public static final String ACTION_REQUEST_HEALTH_PERMISSIONS =
215             "android.health.connect.action.REQUEST_HEALTH_PERMISSIONS";
216 
217     /**
218      * Activity action: Launch UI to health connect home settings screen.
219      *
220      * <p>shows a list of recent apps that accessed (e.g. read/write) health data and allows the
221      * user to access health permissions and health data.
222      *
223      * @hide
224      */
225     @SystemApi
226     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
227     public static final String ACTION_HEALTH_HOME_SETTINGS =
228             "android.health.connect.action.HEALTH_HOME_SETTINGS";
229 
230     /**
231      * Activity action: Launch UI to show and manage (e.g. delete/export) health data.
232      *
233      * <p>shows a list of health data categories and actions to manage (e.g. delete/export) health
234      * data.
235      *
236      * @hide
237      */
238     @SystemApi
239     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
240     public static final String ACTION_MANAGE_HEALTH_DATA =
241             "android.health.connect.action.MANAGE_HEALTH_DATA";
242 
243     /**
244      * Activity action: Display information regarding migration - e.g. asking the user to take some
245      * action (e.g. update the system) so that migration can take place.
246      *
247      * <p><b>Note:</b> Callers of the migration APIs must handle this intent.
248      *
249      * @hide
250      */
251     @SystemApi
252     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
253     public static final String ACTION_SHOW_MIGRATION_INFO =
254             "android.health.connect.action.SHOW_MIGRATION_INFO";
255 
256     /**
257      * Broadcast Action: Health Connect is ready to accept migrated data.
258      *
259      * <p class="note">This broadcast is explicitly sent to Health Connect migration aware
260      * applications to prompt them to start/continue HC data migration. Migration aware applications
261      * are those that both hold {@code android.permission.MIGRATE_HEALTH_CONNECT_DATA} and handle
262      * {@code android.health.connect.action.SHOW_MIGRATION_INFO}.
263      *
264      * <p class="note">This is a protected intent that can only be sent by the system.
265      *
266      * @hide
267      */
268     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
269     @SystemApi
270     public static final String ACTION_HEALTH_CONNECT_MIGRATION_READY =
271             "android.health.connect.action.HEALTH_CONNECT_MIGRATION_READY";
272 
273     /**
274      * Unknown download state considered to be the default download state.
275      *
276      * <p>See also {@link #updateDataDownloadState}
277      *
278      * @hide
279      */
280     @SystemApi public static final int DATA_DOWNLOAD_STATE_UNKNOWN = 0;
281 
282     /**
283      * Indicates that the download has started.
284      *
285      * <p>See also {@link #updateDataDownloadState}
286      *
287      * @hide
288      */
289     @SystemApi public static final int DATA_DOWNLOAD_STARTED = 1;
290 
291     /**
292      * Indicates that the download is being retried.
293      *
294      * <p>See also {@link #updateDataDownloadState}
295      *
296      * @hide
297      */
298     @SystemApi public static final int DATA_DOWNLOAD_RETRY = 2;
299 
300     /**
301      * Indicates that the download has failed.
302      *
303      * <p>See also {@link #updateDataDownloadState}
304      *
305      * @hide
306      */
307     @SystemApi public static final int DATA_DOWNLOAD_FAILED = 3;
308 
309     /**
310      * Indicates that the download has completed.
311      *
312      * <p>See also {@link HealthConnectManager#updateDataDownloadState}
313      *
314      * @hide
315      */
316     @SystemApi public static final int DATA_DOWNLOAD_COMPLETE = 4;
317 
318     /**
319      * No error during the last data export.
320      *
321      * @hide
322      */
323     @FlaggedApi(FLAG_EXPORT_IMPORT)
324     public static final int DATA_EXPORT_ERROR_NONE = 0;
325 
326     /**
327      * Unknown error during the last data export.
328      *
329      * @hide
330      */
331     @FlaggedApi(FLAG_EXPORT_IMPORT)
332     public static final int DATA_EXPORT_ERROR_UNKNOWN = 1;
333 
334     /**
335      * Indicates that the last export failed because we lost access to the export file location.
336      *
337      * @hide
338      */
339     @FlaggedApi(FLAG_EXPORT_IMPORT)
340     public static final int DATA_EXPORT_LOST_FILE_ACCESS = 2;
341 
342     /**
343      * Activity action: Launch activity exported by client application that handles onboarding to
344      * Health Connect.
345      *
346      * <p>Health Connect will invoke this intent whenever the user attempts to connect an app that
347      * has exported an activity that responds to this intent. The launched activity is responsible
348      * for making permission requests and any other prerequisites for connecting to Health Connect.
349      *
350      * <p class="note">Applications exporting an activity that is launched by this intent must also
351      * guard it with {@link HealthPermissions#START_ONBOARDING} so that only the system can launch
352      * it.
353      *
354      * @hide
355      */
356     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
357     public static final String ACTION_SHOW_ONBOARDING =
358             "android.health.connect.action.SHOW_ONBOARDING";
359 
360     private static final String TAG = "HealthConnectManager";
361     private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health.";
362 
363     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
364     private static volatile Set<String> sHealthPermissions;
365 
366     private final Context mContext;
367     private final IHealthConnectService mService;
368     private final InternalExternalRecordConverter mInternalExternalRecordConverter;
369 
370     /** @hide */
HealthConnectManager(@onNull Context context, @NonNull IHealthConnectService service)371     HealthConnectManager(@NonNull Context context, @NonNull IHealthConnectService service) {
372         mContext = context;
373         mService = service;
374         mInternalExternalRecordConverter = InternalExternalRecordConverter.getInstance();
375     }
376 
377     /**
378      * Grant a runtime permission to an application which the application does not already have. The
379      * permission must have been requested by the application. If the application is not allowed to
380      * hold the permission, a {@link java.lang.SecurityException} is thrown. If the package or
381      * permission is invalid, a {@link java.lang.IllegalArgumentException} is thrown.
382      *
383      * <p><b>Note:</b> This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET}.
384      *
385      * @hide
386      */
387     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
388     @UserHandleAware
grantHealthPermission(@onNull String packageName, @NonNull String permissionName)389     public void grantHealthPermission(@NonNull String packageName, @NonNull String permissionName) {
390         try {
391             mService.grantHealthPermission(packageName, permissionName, mContext.getUser());
392         } catch (RemoteException e) {
393             throw e.rethrowFromSystemServer();
394         }
395     }
396 
397     /**
398      * Revoke a health permission that was previously granted by {@link
399      * #grantHealthPermission(String, String)} The permission must have been requested by the
400      * application. If the application is not allowed to hold the permission, a {@link
401      * java.lang.SecurityException} is thrown. If the package or permission is invalid, a {@link
402      * java.lang.IllegalArgumentException} is thrown.
403      *
404      * <p><b>Note:</b> This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET} or {@code
405      * PackageManager.FLAG_PERMISSION_USER_FIXED} based on the number of revocations of a particular
406      * permission for a package.
407      *
408      * @hide
409      */
410     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
411     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
412     @UserHandleAware
revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason)413     public void revokeHealthPermission(
414             @NonNull String packageName, @NonNull String permissionName, @Nullable String reason) {
415         try {
416             mService.revokeHealthPermission(
417                     packageName, permissionName, reason, mContext.getUser());
418         } catch (RemoteException e) {
419             throw e.rethrowFromSystemServer();
420         }
421     }
422 
423     /**
424      * Revokes all health permissions that were previously granted by {@link
425      * #grantHealthPermission(String, String)} If the package is invalid, a {@link
426      * java.lang.IllegalArgumentException} is thrown.
427      *
428      * @hide
429      */
430     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
431     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
432     @UserHandleAware
revokeAllHealthPermissions(@onNull String packageName, @Nullable String reason)433     public void revokeAllHealthPermissions(@NonNull String packageName, @Nullable String reason) {
434         try {
435             mService.revokeAllHealthPermissions(packageName, reason, mContext.getUser());
436         } catch (RemoteException e) {
437             throw e.rethrowFromSystemServer();
438         }
439     }
440 
441     /**
442      * Returns a list of health permissions that were previously granted by {@link
443      * #grantHealthPermission(String, String)}.
444      *
445      * @hide
446      */
447     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
448     @UserHandleAware
getGrantedHealthPermissions(@onNull String packageName)449     public List<String> getGrantedHealthPermissions(@NonNull String packageName) {
450         try {
451             return mService.getGrantedHealthPermissions(packageName, mContext.getUser());
452         } catch (RemoteException e) {
453             throw e.rethrowFromSystemServer();
454         }
455     }
456 
457     /**
458      * Returns permission flags for the given package name and Health permissions.
459      *
460      * <p>This is equivalent to calling {@link PackageManager#getPermissionFlags(String, String,
461      * UserHandle)} for each provided permission except it throws an exception for non-Health or
462      * undeclared permissions. Flag masks listed in {@link PackageManager#MASK_PERMISSION_FLAGS_ALL}
463      * can be used to check the flag values.
464      *
465      * <p>Returned flags for invalid, non-Health or undeclared permissions are equal to zero.
466      *
467      * @return a map which contains all requested permissions as keys and corresponding flags as
468      *     values.
469      * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not
470      *     Health permissions or not declared by the app.
471      * @throws NullPointerException if any of the arguments is {@code null}.
472      * @throws SecurityException if the caller doesn't possess {@code
473      *     android.permission.MANAGE_HEALTH_PERMISSIONS}.
474      * @hide
475      */
476     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
477     @UserHandleAware
getHealthPermissionsFlags( @onNull String packageName, @NonNull List<String> permissions)478     public Map<String, Integer> getHealthPermissionsFlags(
479             @NonNull String packageName, @NonNull List<String> permissions) {
480         try {
481             return mService.getHealthPermissionsFlags(packageName, mContext.getUser(), permissions);
482         } catch (RemoteException e) {
483             throw e.rethrowFromSystemServer();
484         }
485     }
486 
487     /**
488      * Sets/clears {@link PackageManager#FLAG_PERMISSION_USER_FIXED} for given health permissions.
489      *
490      * @param value whether to set or clear the flag, {@code true} means set, {@code false} - clear.
491      * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not
492      *     Health permissions or not declared by the app.
493      * @throws NullPointerException if any of the arguments is {@code null}.
494      * @throws SecurityException if the caller doesn't possess {@code
495      *     android.permission.MANAGE_HEALTH_PERMISSIONS}.
496      * @hide
497      */
498     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
499     @UserHandleAware
setHealthPermissionsUserFixedFlagValue( @onNull String packageName, @NonNull List<String> permissions, boolean value)500     public void setHealthPermissionsUserFixedFlagValue(
501             @NonNull String packageName, @NonNull List<String> permissions, boolean value) {
502         try {
503             mService.setHealthPermissionsUserFixedFlagValue(
504                     packageName, mContext.getUser(), permissions, value);
505         } catch (RemoteException e) {
506             throw e.rethrowFromSystemServer();
507         }
508     }
509 
510     /**
511      * Returns the date from which an app have access to the historical health data. Returns null if
512      * the package doesn't have historical access date.
513      *
514      * @hide
515      */
516     @RequiresPermission(HealthPermissions.MANAGE_HEALTH_PERMISSIONS)
517     @UserHandleAware
518     @Nullable
getHealthDataHistoricalAccessStartDate(@onNull String packageName)519     public Instant getHealthDataHistoricalAccessStartDate(@NonNull String packageName) {
520         try {
521             long dateMilli =
522                     mService.getHistoricalAccessStartDateInMilliseconds(
523                             packageName, mContext.getUser());
524             if (dateMilli == DEFAULT_LONG) {
525                 return null;
526             } else {
527                 return Instant.ofEpochMilli(dateMilli);
528             }
529         } catch (RemoteException e) {
530             throw e.rethrowFromSystemServer();
531         }
532     }
533 
534     /**
535      * Inserts {@code records} into the HealthConnect database. The records returned in {@link
536      * InsertRecordsResponse} contains the unique IDs of the input records. The values are in same
537      * order as {@code records}. In case of an error or a permission failure the HealthConnect
538      * service, {@link OutcomeReceiver#onError} will be invoked with a {@link
539      * HealthConnectException}.
540      *
541      * @param records list of records to be inserted.
542      * @param executor Executor on which to invoke the callback.
543      * @param callback Callback to receive result of performing this operation.
544      * @throws RuntimeException for internal errors
545      */
insertRecords( @onNull List<Record> records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback)546     public void insertRecords(
547             @NonNull List<Record> records,
548             @NonNull @CallbackExecutor Executor executor,
549             @NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback) {
550         Objects.requireNonNull(records);
551         Objects.requireNonNull(executor);
552         Objects.requireNonNull(callback);
553         try {
554             // Unset any set ids for insert. This is to prevent random string ids from creating
555             // illegal argument exception.
556             records.forEach((record) -> record.getMetadata().setId(""));
557             List<RecordInternal<?>> recordInternals =
558                     records.stream()
559                             .map(
560                                     record ->
561                                             record.toRecordInternal()
562                                                     .setPackageName(mContext.getPackageName()))
563                             .collect(Collectors.toList());
564             mService.insertRecords(
565                     mContext.getAttributionSource(),
566                     new RecordsParcel(recordInternals),
567                     new IInsertRecordsResponseCallback.Stub() {
568                         @Override
569                         public void onResult(InsertRecordsResponseParcel parcel) {
570                             Binder.clearCallingIdentity();
571                             executor.execute(
572                                     () ->
573                                             callback.onResult(
574                                                     new InsertRecordsResponse(
575                                                             toExternalRecordsWithUuids(
576                                                                     recordInternals,
577                                                                     parcel.getUids()))));
578                         }
579 
580                         @Override
581                         public void onError(HealthConnectExceptionParcel exception) {
582                             returnError(executor, exception, callback);
583                         }
584                     });
585         } catch (RemoteException e) {
586             throw e.rethrowFromSystemServer();
587         }
588     }
589 
590     /**
591      * Get aggregations corresponding to {@code request}.
592      *
593      * @param <T> Result type of the aggregation.
594      *     <p>Note:
595      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
596      *     typed in nature.
597      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
598      * @param request request for different aggregation.
599      * @param executor Executor on which to invoke the callback.
600      * @param callback Callback to receive result of performing this operation.
601      * @see AggregateRecordsResponse#get
602      */
603     @NonNull
604     @SuppressWarnings("unchecked")
aggregate( @onNull AggregateRecordsRequest<T> request, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<AggregateRecordsResponse<T>, HealthConnectException> callback)605     public <T> void aggregate(
606             @NonNull AggregateRecordsRequest<T> request,
607             @NonNull @CallbackExecutor Executor executor,
608             @NonNull
609                     OutcomeReceiver<AggregateRecordsResponse<T>, HealthConnectException> callback) {
610         Objects.requireNonNull(request);
611         Objects.requireNonNull(executor);
612         Objects.requireNonNull(callback);
613         try {
614             mService.aggregateRecords(
615                     mContext.getAttributionSource(),
616                     new AggregateDataRequestParcel(request),
617                     new IAggregateRecordsResponseCallback.Stub() {
618                         @Override
619                         public void onResult(AggregateDataResponseParcel parcel) {
620                             Binder.clearCallingIdentity();
621                             try {
622                                 executor.execute(
623                                         () ->
624                                                 callback.onResult(
625                                                         (AggregateRecordsResponse<T>)
626                                                                 parcel.getAggregateDataResponse()));
627                             } catch (Exception exception) {
628                                 callback.onError(
629                                         new HealthConnectException(
630                                                 HealthConnectException.ERROR_INTERNAL));
631                             }
632                         }
633 
634                         @Override
635                         public void onError(HealthConnectExceptionParcel exception) {
636                             returnError(executor, exception, callback);
637                         }
638                     });
639         } catch (ClassCastException classCastException) {
640             returnError(
641                     executor,
642                     new HealthConnectExceptionParcel(
643                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
644                     callback);
645         } catch (RemoteException e) {
646             throw e.rethrowFromSystemServer();
647         }
648     }
649 
650     /**
651      * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped
652      * by concrete intervals of time, for example 5 Hrs, 10 Hrs etc.
653      *
654      * @param <T> Result type of the aggregation.
655      *     <p>Note:
656      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
657      *     typed in nature.
658      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
659      * @param request request for different aggregation.
660      * @param duration Duration on which to group by results
661      * @param executor Executor on which to invoke the callback.
662      * @param callback Callback to receive result of performing this operation.
663      * @see HealthConnectManager#aggregateGroupByPeriod
664      */
665     @SuppressWarnings("unchecked")
aggregateGroupByDuration( @onNull AggregateRecordsRequest<T> request, @NonNull Duration duration, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List<AggregateRecordsGroupedByDurationResponse<T>>, HealthConnectException> callback)666     public <T> void aggregateGroupByDuration(
667             @NonNull AggregateRecordsRequest<T> request,
668             @NonNull Duration duration,
669             @NonNull @CallbackExecutor Executor executor,
670             @NonNull
671                     OutcomeReceiver<
672                                     List<AggregateRecordsGroupedByDurationResponse<T>>,
673                                     HealthConnectException>
674                             callback) {
675         Objects.requireNonNull(request);
676         Objects.requireNonNull(duration);
677         if (duration.toMillis() < 1) {
678             throw new IllegalArgumentException("Duration should be at least 1 millisecond");
679         }
680         Objects.requireNonNull(executor);
681         Objects.requireNonNull(callback);
682         try {
683             mService.aggregateRecords(
684                     mContext.getAttributionSource(),
685                     new AggregateDataRequestParcel(request, duration),
686                     new IAggregateRecordsResponseCallback.Stub() {
687                         @Override
688                         public void onResult(AggregateDataResponseParcel parcel) {
689                             Binder.clearCallingIdentity();
690                             List<AggregateRecordsGroupedByDurationResponse<T>> result =
691                                     new ArrayList<>();
692                             for (AggregateRecordsGroupedByDurationResponse<?>
693                                     aggregateRecordsGroupedByDurationResponse :
694                                             parcel.getAggregateDataResponseGroupedByDuration()) {
695                                 result.add(
696                                         (AggregateRecordsGroupedByDurationResponse<T>)
697                                                 aggregateRecordsGroupedByDurationResponse);
698                             }
699                             executor.execute(() -> callback.onResult(result));
700                         }
701 
702                         @Override
703                         public void onError(HealthConnectExceptionParcel exception) {
704                             returnError(executor, exception, callback);
705                         }
706                     });
707         } catch (ClassCastException classCastException) {
708             returnError(
709                     executor,
710                     new HealthConnectExceptionParcel(
711                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
712                     callback);
713         } catch (RemoteException e) {
714             throw e.rethrowFromSystemServer();
715         }
716     }
717 
718     /**
719      * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped
720      * by number of days. This API handles changes in {@link ZoneOffset} when computing the data on
721      * a per-day basis.
722      *
723      * @param <T> Result type of the aggregation.
724      *     <p>Note:
725      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
726      *     typed in nature.
727      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
728      * @param request Request for different aggregation.
729      * @param period Period on which to group by results
730      * @param executor Executor on which to invoke the callback.
731      * @param callback Callback to receive result of performing this operation.
732      * @see AggregateRecordsGroupedByPeriodResponse#get
733      * @see HealthConnectManager#aggregateGroupByDuration
734      */
735     @SuppressWarnings("unchecked")
aggregateGroupByPeriod( @onNull AggregateRecordsRequest<T> request, @NonNull Period period, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List<AggregateRecordsGroupedByPeriodResponse<T>>, HealthConnectException> callback)736     public <T> void aggregateGroupByPeriod(
737             @NonNull AggregateRecordsRequest<T> request,
738             @NonNull Period period,
739             @NonNull @CallbackExecutor Executor executor,
740             @NonNull
741                     OutcomeReceiver<
742                                     List<AggregateRecordsGroupedByPeriodResponse<T>>,
743                                     HealthConnectException>
744                             callback) {
745         Objects.requireNonNull(request);
746         Objects.requireNonNull(period);
747         if (period == Period.ZERO) {
748             throw new IllegalArgumentException("Period duration should be at least a day");
749         }
750         Objects.requireNonNull(executor);
751         Objects.requireNonNull(callback);
752         try {
753             mService.aggregateRecords(
754                     mContext.getAttributionSource(),
755                     new AggregateDataRequestParcel(request, period),
756                     new IAggregateRecordsResponseCallback.Stub() {
757                         @Override
758                         public void onResult(AggregateDataResponseParcel parcel) {
759                             Binder.clearCallingIdentity();
760                             List<AggregateRecordsGroupedByPeriodResponse<T>> result =
761                                     new ArrayList<>();
762                             for (AggregateRecordsGroupedByPeriodResponse<?>
763                                     aggregateRecordsGroupedByPeriodResponse :
764                                             parcel.getAggregateDataResponseGroupedByPeriod()) {
765                                 result.add(
766                                         (AggregateRecordsGroupedByPeriodResponse<T>)
767                                                 aggregateRecordsGroupedByPeriodResponse);
768                             }
769 
770                             executor.execute(() -> callback.onResult(result));
771                         }
772 
773                         @Override
774                         public void onError(HealthConnectExceptionParcel exception) {
775                             returnError(executor, exception, callback);
776                         }
777                     });
778         } catch (ClassCastException classCastException) {
779             returnError(
780                     executor,
781                     new HealthConnectExceptionParcel(
782                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
783                     callback);
784         } catch (RemoteException e) {
785             throw e.rethrowFromSystemServer();
786         }
787     }
788 
789     /**
790      * Deletes records based on the {@link DeleteUsingFiltersRequest}. This is only to be used by
791      * health connect controller APK(s). Ids that don't exist will be ignored.
792      *
793      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
794      *
795      * @param request Request based on which to perform delete operation
796      * @param executor Executor on which to invoke the callback.
797      * @param callback Callback to receive result of performing this operation.
798      * @hide
799      */
800     @SystemApi
801     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
deleteRecords( @onNull DeleteUsingFiltersRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)802     public void deleteRecords(
803             @NonNull DeleteUsingFiltersRequest request,
804             @NonNull Executor executor,
805             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
806         Objects.requireNonNull(request);
807         Objects.requireNonNull(executor);
808         Objects.requireNonNull(callback);
809 
810         try {
811             mService.deleteUsingFilters(
812                     mContext.getAttributionSource(),
813                     new DeleteUsingFiltersRequestParcel(request),
814                     new IEmptyResponseCallback.Stub() {
815                         @Override
816                         public void onResult() {
817                             executor.execute(() -> callback.onResult(null));
818                         }
819 
820                         @Override
821                         public void onError(HealthConnectExceptionParcel exception) {
822                             returnError(executor, exception, callback);
823                         }
824                     });
825         } catch (RemoteException remoteException) {
826             remoteException.rethrowFromSystemServer();
827         }
828     }
829 
830     /**
831      * Deletes records based on {@link RecordIdFilter}.
832      *
833      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
834      *
835      * @param recordIds recordIds on which to perform delete operation.
836      * @param executor Executor on which to invoke the callback.
837      * @param callback Callback to receive result of performing this operation.
838      * @throws IllegalArgumentException if {@code recordIds is empty}
839      */
deleteRecords( @onNull List<RecordIdFilter> recordIds, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)840     public void deleteRecords(
841             @NonNull List<RecordIdFilter> recordIds,
842             @NonNull Executor executor,
843             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
844         Objects.requireNonNull(recordIds);
845         Objects.requireNonNull(executor);
846         Objects.requireNonNull(callback);
847 
848         if (recordIds.isEmpty()) {
849             throw new IllegalArgumentException("record ids can't be empty");
850         }
851 
852         try {
853             mService.deleteUsingFiltersForSelf(
854                     mContext.getAttributionSource(),
855                     new DeleteUsingFiltersRequestParcel(
856                             new RecordIdFiltersParcel(recordIds), mContext.getPackageName()),
857                     new IEmptyResponseCallback.Stub() {
858                         @Override
859                         public void onResult() {
860                             executor.execute(() -> callback.onResult(null));
861                         }
862 
863                         @Override
864                         public void onError(HealthConnectExceptionParcel exception) {
865                             returnError(executor, exception, callback);
866                         }
867                     });
868         } catch (RemoteException remoteException) {
869             remoteException.rethrowFromSystemServer();
870         }
871     }
872 
873     /**
874      * Deletes records based on the {@link TimeRangeFilter}.
875      *
876      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
877      *
878      * @param recordType recordType to perform delete operation on.
879      * @param timeRangeFilter time filter based on which to delete the records.
880      * @param executor Executor on which to invoke the callback.
881      * @param callback Callback to receive result of performing this operation.
882      */
deleteRecords( @onNull Class<? extends Record> recordType, @NonNull TimeRangeFilter timeRangeFilter, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)883     public void deleteRecords(
884             @NonNull Class<? extends Record> recordType,
885             @NonNull TimeRangeFilter timeRangeFilter,
886             @NonNull Executor executor,
887             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
888         Objects.requireNonNull(recordType);
889         Objects.requireNonNull(timeRangeFilter);
890         Objects.requireNonNull(executor);
891         Objects.requireNonNull(callback);
892 
893         try {
894             mService.deleteUsingFiltersForSelf(
895                     mContext.getAttributionSource(),
896                     new DeleteUsingFiltersRequestParcel(
897                             new DeleteUsingFiltersRequest.Builder()
898                                     .addDataOrigin(
899                                             new DataOrigin.Builder()
900                                                     .setPackageName(mContext.getPackageName())
901                                                     .build())
902                                     .addRecordType(recordType)
903                                     .setTimeRangeFilter(timeRangeFilter)
904                                     .build()),
905                     new IEmptyResponseCallback.Stub() {
906                         @Override
907                         public void onResult() {
908                             executor.execute(() -> callback.onResult(null));
909                         }
910 
911                         @Override
912                         public void onError(HealthConnectExceptionParcel exception) {
913                             returnError(executor, exception, callback);
914                         }
915                     });
916         } catch (RemoteException remoteException) {
917             remoteException.rethrowFromSystemServer();
918         }
919     }
920 
921     /**
922      * Get change logs post the time when {@code token} was generated.
923      *
924      * @param changeLogsRequest The token from {@link HealthConnectManager#getChangeLogToken}.
925      * @param executor Executor on which to invoke the callback.
926      * @param callback Callback to receive result of performing this operation.
927      * @see HealthConnectManager#getChangeLogToken
928      */
getChangeLogs( @onNull ChangeLogsRequest changeLogsRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback)929     public void getChangeLogs(
930             @NonNull ChangeLogsRequest changeLogsRequest,
931             @NonNull @CallbackExecutor Executor executor,
932             @NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback) {
933         Objects.requireNonNull(changeLogsRequest);
934         Objects.requireNonNull(executor);
935         Objects.requireNonNull(callback);
936 
937         try {
938             mService.getChangeLogs(
939                     mContext.getAttributionSource(),
940                     changeLogsRequest,
941                     new IChangeLogsResponseCallback.Stub() {
942                         @Override
943                         public void onResult(ChangeLogsResponse parcel) {
944                             Binder.clearCallingIdentity();
945                             executor.execute(() -> callback.onResult(parcel));
946                         }
947 
948                         @Override
949                         public void onError(HealthConnectExceptionParcel exception) {
950                             returnError(executor, exception, callback);
951                         }
952                     });
953         } catch (ClassCastException invalidArgumentException) {
954             callback.onError(
955                     new HealthConnectException(
956                             HealthConnectException.ERROR_INVALID_ARGUMENT,
957                             invalidArgumentException.getMessage()));
958         } catch (RemoteException e) {
959             throw e.rethrowFromSystemServer();
960         }
961     }
962 
963     /**
964      * Get token for {HealthConnectManager#getChangeLogs}. Changelogs requested corresponding to
965      * this token will be post the time this token was generated by the system all items that match
966      * the given filters.
967      *
968      * <p>Tokens from this request are to be passed to {HealthConnectManager#getChangeLogs}
969      *
970      * @param request A request to get changelog token
971      * @param executor Executor on which to invoke the callback.
972      * @param callback Callback to receive result of performing this operation.
973      */
getChangeLogToken( @onNull ChangeLogTokenRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<ChangeLogTokenResponse, HealthConnectException> callback)974     public void getChangeLogToken(
975             @NonNull ChangeLogTokenRequest request,
976             @NonNull Executor executor,
977             @NonNull OutcomeReceiver<ChangeLogTokenResponse, HealthConnectException> callback) {
978         try {
979             mService.getChangeLogToken(
980                     mContext.getAttributionSource(),
981                     request,
982                     new IGetChangeLogTokenCallback.Stub() {
983                         @Override
984                         public void onResult(ChangeLogTokenResponse parcel) {
985                             Binder.clearCallingIdentity();
986                             executor.execute(() -> callback.onResult(parcel));
987                         }
988 
989                         @Override
990                         public void onError(HealthConnectExceptionParcel exception) {
991                             returnError(executor, exception, callback);
992                         }
993                     });
994         } catch (RemoteException e) {
995             throw e.rethrowFromSystemServer();
996         }
997     }
998 
999     /**
1000      * Fetch the data priority order of the contributing {@link DataOrigin} for {@code
1001      * dataCategory}.
1002      *
1003      * @param dataCategory {@link HealthDataCategory} for which to get the priority order
1004      * @param executor Executor on which to invoke the callback.
1005      * @param callback Callback to receive result of performing this operation.
1006      * @hide
1007      */
1008     @SystemApi
1009     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
fetchDataOriginsPriorityOrder( @ealthDataCategory.Type int dataCategory, @NonNull Executor executor, @NonNull OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, HealthConnectException> callback)1010     public void fetchDataOriginsPriorityOrder(
1011             @HealthDataCategory.Type int dataCategory,
1012             @NonNull Executor executor,
1013             @NonNull
1014                     OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, HealthConnectException>
1015                             callback) {
1016         try {
1017             mService.getCurrentPriority(
1018                     mContext.getPackageName(),
1019                     dataCategory,
1020                     new IGetPriorityResponseCallback.Stub() {
1021                         @Override
1022                         public void onResult(GetPriorityResponseParcel response) {
1023                             Binder.clearCallingIdentity();
1024                             executor.execute(
1025                                     () -> callback.onResult(response.getPriorityResponse()));
1026                         }
1027 
1028                         @Override
1029                         public void onError(HealthConnectExceptionParcel exception) {
1030                             returnError(executor, exception, callback);
1031                         }
1032                     });
1033         } catch (RemoteException e) {
1034             throw e.rethrowFromSystemServer();
1035         }
1036     }
1037 
1038     /**
1039      * Updates the priority order of the apps as per {@code request}
1040      *
1041      * @param request new priority order update request
1042      * @param executor Executor on which to invoke the callback.
1043      * @param callback Callback to receive result of performing this operation.
1044      * @hide
1045      */
1046     @SystemApi
1047     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
updateDataOriginPriorityOrder( @onNull UpdateDataOriginPriorityOrderRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1048     public void updateDataOriginPriorityOrder(
1049             @NonNull UpdateDataOriginPriorityOrderRequest request,
1050             @NonNull Executor executor,
1051             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1052         try {
1053             mService.updatePriority(
1054                     mContext.getPackageName(),
1055                     new UpdatePriorityRequestParcel(request),
1056                     new IEmptyResponseCallback.Stub() {
1057                         @Override
1058                         public void onResult() {
1059                             Binder.clearCallingIdentity();
1060                             executor.execute(() -> callback.onResult(null));
1061                         }
1062 
1063                         @Override
1064                         public void onError(HealthConnectExceptionParcel exception) {
1065                             returnError(executor, exception, callback);
1066                         }
1067                     });
1068         } catch (RemoteException e) {
1069             throw e.rethrowFromSystemServer();
1070         }
1071     }
1072 
1073     /**
1074      * Retrieves {@link RecordTypeInfoResponse} for each RecordType.
1075      *
1076      * @param executor Executor on which to invoke the callback.
1077      * @param callback Callback to receive result of performing this operation.
1078      * @hide
1079      */
1080     @SystemApi
1081     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryAllRecordTypesInfo( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver< Map<Class<? extends Record>, RecordTypeInfoResponse>, HealthConnectException> callback)1082     public void queryAllRecordTypesInfo(
1083             @NonNull @CallbackExecutor Executor executor,
1084             @NonNull
1085                     OutcomeReceiver<
1086                                     Map<Class<? extends Record>, RecordTypeInfoResponse>,
1087                                     HealthConnectException>
1088                             callback) {
1089         Objects.requireNonNull(executor);
1090         Objects.requireNonNull(callback);
1091         try {
1092             mService.queryAllRecordTypesInfo(
1093                     new IRecordTypeInfoResponseCallback.Stub() {
1094                         @Override
1095                         public void onResult(RecordTypeInfoResponseParcel parcel) {
1096                             Binder.clearCallingIdentity();
1097                             executor.execute(
1098                                     () -> callback.onResult(parcel.getRecordTypeInfoResponses()));
1099                         }
1100 
1101                         @Override
1102                         public void onError(HealthConnectExceptionParcel exception) {
1103                             returnError(executor, exception, callback);
1104                         }
1105                     });
1106         } catch (RemoteException e) {
1107             throw e.rethrowFromSystemServer();
1108         }
1109     }
1110 
1111     /**
1112      * Returns currently set auto delete period for this user.
1113      *
1114      * <p>If you are calling this function for the first time after a user unlock, this might take
1115      * some time so consider calling this on a thread.
1116      *
1117      * @return Auto delete period in days, 0 is returned if auto delete period is not set.
1118      * @throws RuntimeException for internal errors
1119      * @hide
1120      */
1121     @SystemApi
1122     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
1123     @IntRange(from = 0, to = 7300)
getRecordRetentionPeriodInDays()1124     public int getRecordRetentionPeriodInDays() {
1125         try {
1126             return mService.getRecordRetentionPeriodInDays(mContext.getUser());
1127         } catch (RemoteException e) {
1128             throw e.rethrowFromSystemServer();
1129         }
1130     }
1131 
1132     /**
1133      * Sets auto delete period (for all the records to be automatically deleted) for this user.
1134      *
1135      * <p>Note: The max value of auto delete period can be 7300 i.e. ~20 years
1136      *
1137      * @param days Auto period to be set in days. Use 0 to unset this value.
1138      * @param executor Executor on which to invoke the callback.
1139      * @param callback Callback to receive result of performing this operation.
1140      * @throws RuntimeException for internal errors
1141      * @throws IllegalArgumentException if {@code days} is not between 0 and 7300
1142      * @hide
1143      */
1144     @SystemApi
1145     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
setRecordRetentionPeriodInDays( @ntRangefrom = 0, to = 7300) int days, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1146     public void setRecordRetentionPeriodInDays(
1147             @IntRange(from = 0, to = 7300) int days,
1148             @NonNull Executor executor,
1149             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1150         Objects.requireNonNull(executor);
1151         Objects.requireNonNull(callback);
1152 
1153         if (days < 0 || days > 7300) {
1154             throw new IllegalArgumentException("days should be between " + 0 + " and " + 7300);
1155         }
1156 
1157         try {
1158             mService.setRecordRetentionPeriodInDays(
1159                     days,
1160                     mContext.getUser(),
1161                     new IEmptyResponseCallback.Stub() {
1162                         @Override
1163                         public void onResult() {
1164                             Binder.clearCallingIdentity();
1165                             executor.execute(() -> callback.onResult(null));
1166                         }
1167 
1168                         @Override
1169                         public void onError(HealthConnectExceptionParcel exception) {
1170                             returnError(executor, exception, callback);
1171                         }
1172                     });
1173         } catch (RemoteException e) {
1174             e.rethrowFromSystemServer();
1175         }
1176     }
1177 
1178     /**
1179      * Returns a list of access logs with package name and its access time for each record type.
1180      *
1181      * @param executor Executor on which to invoke the callback.
1182      * @param callback Callback to receive result of performing this operation.
1183      * @hide
1184      */
1185     @SystemApi
1186     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryAccessLogs( @onNull Executor executor, @NonNull OutcomeReceiver<List<AccessLog>, HealthConnectException> callback)1187     public void queryAccessLogs(
1188             @NonNull Executor executor,
1189             @NonNull OutcomeReceiver<List<AccessLog>, HealthConnectException> callback) {
1190         Objects.requireNonNull(executor);
1191         Objects.requireNonNull(callback);
1192         try {
1193             mService.queryAccessLogs(
1194                     mContext.getPackageName(),
1195                     new IAccessLogsResponseCallback.Stub() {
1196                         @Override
1197                         public void onResult(AccessLogsResponseParcel parcel) {
1198                             Binder.clearCallingIdentity();
1199                             executor.execute(() -> callback.onResult(parcel.getAccessLogs()));
1200                         }
1201 
1202                         @Override
1203                         public void onError(HealthConnectExceptionParcel exception) {
1204                             returnError(executor, exception, callback);
1205                         }
1206                     });
1207         } catch (RemoteException e) {
1208             throw e.rethrowFromSystemServer();
1209         }
1210     }
1211 
1212     /**
1213      * API to read records based on {@link ReadRecordsRequestUsingFilters} or {@link
1214      * ReadRecordsRequestUsingIds}
1215      *
1216      * <p>Number of records returned by this API will depend based on below factors:
1217      *
1218      * <p>When an app with read permission allowed calls the API from background then it will be
1219      * able to read only its own inserted records and will not get records inserted by other apps.
1220      * This may be less than the total records present for the record type.
1221      *
1222      * <p>When an app with read permission allowed calls the API from foreground then it will be
1223      * able to read all records for the record type.
1224      *
1225      * <p>App with only write permission but no read permission allowed will be able to read only
1226      * its own inserted records both when in foreground or background.
1227      *
1228      * <p>An app without both read and write permissions will not be able to read any record and the
1229      * API will throw Security Exception.
1230      *
1231      * @param request Read request based on {@link ReadRecordsRequestUsingFilters} or {@link
1232      *     ReadRecordsRequestUsingIds}
1233      * @param executor Executor on which to invoke the callback.
1234      * @param callback Callback to receive result of performing this operation.
1235      * @throws IllegalArgumentException if request page size set is more than 5000 in {@link
1236      *     ReadRecordsRequestUsingFilters}
1237      * @throws SecurityException if app without read or write permission tries to read.
1238      */
readRecords( @onNull ReadRecordsRequest<T> request, @NonNull Executor executor, @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback)1239     public <T extends Record> void readRecords(
1240             @NonNull ReadRecordsRequest<T> request,
1241             @NonNull Executor executor,
1242             @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
1243         Objects.requireNonNull(request);
1244         Objects.requireNonNull(executor);
1245         Objects.requireNonNull(callback);
1246         try {
1247             mService.readRecords(
1248                     mContext.getAttributionSource(),
1249                     request.toReadRecordsRequestParcel(),
1250                     getReadCallback(executor, callback));
1251         } catch (RemoteException remoteException) {
1252             remoteException.rethrowFromSystemServer();
1253         }
1254     }
1255 
1256     /**
1257      * Updates {@code records} into the HealthConnect database. In case of an error or a permission
1258      * failure the HealthConnect service, {@link OutcomeReceiver#onError} will be invoked with a
1259      * {@link HealthConnectException}.
1260      *
1261      * <p>In case the input record to be updated does not exist in the database or the caller is not
1262      * the owner of the record then {@link HealthConnectException#ERROR_INVALID_ARGUMENT} will be
1263      * thrown.
1264      *
1265      * @param records list of records to be updated.
1266      * @param executor Executor on which to invoke the callback.
1267      * @param callback Callback to receive result of performing this operation.
1268      * @throws IllegalArgumentException if at least one of the records is missing both
1269      *     ClientRecordID and UUID.
1270      */
updateRecords( @onNull List<Record> records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1271     public void updateRecords(
1272             @NonNull List<Record> records,
1273             @NonNull @CallbackExecutor Executor executor,
1274             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1275         Objects.requireNonNull(records);
1276         Objects.requireNonNull(executor);
1277         Objects.requireNonNull(callback);
1278         try {
1279             List<RecordInternal<?>> recordInternals =
1280                     records.stream().map(Record::toRecordInternal).collect(Collectors.toList());
1281             // Verify if the input record has clientRecordId or UUID.
1282             for (RecordInternal<?> recordInternal : recordInternals) {
1283                 if ((recordInternal.getClientRecordId() == null
1284                                 || recordInternal.getClientRecordId().isEmpty())
1285                         && recordInternal.getUuid() == null) {
1286                     throw new IllegalArgumentException(
1287                             "At least one of the records is missing both ClientRecordID"
1288                                     + " and UUID. RecordType of the input: "
1289                                     + recordInternal.getRecordType());
1290                 }
1291             }
1292 
1293             mService.updateRecords(
1294                     mContext.getAttributionSource(),
1295                     new RecordsParcel(recordInternals),
1296                     new IEmptyResponseCallback.Stub() {
1297                         @Override
1298                         public void onResult() {
1299                             Binder.clearCallingIdentity();
1300                             executor.execute(() -> callback.onResult(null));
1301                         }
1302 
1303                         @Override
1304                         public void onError(HealthConnectExceptionParcel exception) {
1305                             Binder.clearCallingIdentity();
1306                             callback.onError(exception.getHealthConnectException());
1307                         }
1308                     });
1309         } catch (ArithmeticException
1310                 | ClassCastException
1311                 | IllegalArgumentException invalidArgumentException) {
1312             throw new IllegalArgumentException(invalidArgumentException);
1313         } catch (RemoteException e) {
1314             throw e.rethrowFromSystemServer();
1315         }
1316     }
1317 
1318     /**
1319      * Returns information, represented by {@code ApplicationInfoResponse}, for all the packages
1320      * that have contributed to the health connect DB. If the application is does not have
1321      * permissions to query other packages, a {@link java.lang.SecurityException} is thrown.
1322      *
1323      * @param executor Executor on which to invoke the callback.
1324      * @param callback Callback to receive result of performing this operation.
1325      * @hide
1326      */
1327     @NonNull
1328     @SystemApi
1329     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getContributorApplicationsInfo( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback)1330     public void getContributorApplicationsInfo(
1331             @NonNull @CallbackExecutor Executor executor,
1332             @NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback) {
1333         Objects.requireNonNull(executor);
1334         Objects.requireNonNull(callback);
1335 
1336         try {
1337             mService.getContributorApplicationsInfo(
1338                     new IApplicationInfoResponseCallback.Stub() {
1339                         @Override
1340                         public void onResult(ApplicationInfoResponseParcel parcel) {
1341                             Binder.clearCallingIdentity();
1342                             executor.execute(
1343                                     () ->
1344                                             callback.onResult(
1345                                                     new ApplicationInfoResponse(
1346                                                             parcel.getAppInfoList())));
1347                         }
1348 
1349                         @Override
1350                         public void onError(HealthConnectExceptionParcel exception) {
1351                             returnError(executor, exception, callback);
1352                         }
1353                     });
1354 
1355         } catch (RemoteException e) {
1356             throw e.rethrowFromSystemServer();
1357         }
1358     }
1359 
1360     /**
1361      * Stages all HealthConnect remote data and returns any errors in a callback. Errors encountered
1362      * for all the files are shared in the provided callback. Any authorization / permissions
1363      * related error is reported to the callback with an empty file name.
1364      *
1365      * <p>The staged data will later be restored (integrated) into the existing Health Connect data.
1366      * Any existing data will not be affected by the staged data.
1367      *
1368      * <p>The file names passed should be the same as the ones on the original device that were
1369      * backed up or are being transferred directly.
1370      *
1371      * <p>If a file already exists in the staged data then it will be replaced. However, note that
1372      * staging data is a one time process. And if the staged data has already been processed then
1373      * any attempt to stage data again will be silently ignored.
1374      *
1375      * <p>The caller is responsible for closing the original file descriptors. The file descriptors
1376      * are duplicated and the originals may be closed by the application at any time after this API
1377      * returns.
1378      *
1379      * <p>The caller should update the data download states using {@link #updateDataDownloadState}
1380      * before calling this API.
1381      *
1382      * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s.
1383      * @param executor The {@link Executor} on which to invoke the callback.
1384      * @param callback The callback which will receive the outcome of this call.
1385      * @hide
1386      */
1387     @SystemApi
1388     @UserHandleAware
1389     @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA)
stageAllHealthConnectRemoteData( @onNull Map<String, ParcelFileDescriptor> pfdsByFileName, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, StageRemoteDataException> callback)1390     public void stageAllHealthConnectRemoteData(
1391             @NonNull Map<String, ParcelFileDescriptor> pfdsByFileName,
1392             @NonNull Executor executor,
1393             @NonNull OutcomeReceiver<Void, StageRemoteDataException> callback)
1394             throws NullPointerException {
1395         Objects.requireNonNull(pfdsByFileName);
1396         Objects.requireNonNull(executor);
1397         Objects.requireNonNull(callback);
1398 
1399         try {
1400             mService.stageAllHealthConnectRemoteData(
1401                     new StageRemoteDataRequest(pfdsByFileName),
1402                     mContext.getUser(),
1403                     new IDataStagingFinishedCallback.Stub() {
1404                         @Override
1405                         public void onResult() {
1406                             Binder.clearCallingIdentity();
1407                             executor.execute(() -> callback.onResult(null));
1408                         }
1409 
1410                         @Override
1411                         public void onError(StageRemoteDataException stageRemoteDataException) {
1412                             Binder.clearCallingIdentity();
1413                             executor.execute(() -> callback.onError(stageRemoteDataException));
1414                         }
1415                     });
1416         } catch (RemoteException e) {
1417             throw e.rethrowFromSystemServer();
1418         }
1419     }
1420 
1421     /**
1422      * Copies all HealthConnect backup data in the passed FDs.
1423      *
1424      * <p>The shared data must later be sent for Backup to cloud or another device.
1425      *
1426      * <p>We are responsible for closing the original file descriptors. The caller must not close
1427      * the FD before that.
1428      *
1429      * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s.
1430      * @hide
1431      */
getAllDataForBackup(@onNull Map<String, ParcelFileDescriptor> pfdsByFileName)1432     public void getAllDataForBackup(@NonNull Map<String, ParcelFileDescriptor> pfdsByFileName) {
1433         Objects.requireNonNull(pfdsByFileName);
1434 
1435         try {
1436             mService.getAllDataForBackup(
1437                     new StageRemoteDataRequest(pfdsByFileName), mContext.getUser());
1438         } catch (RemoteException e) {
1439             throw e.rethrowFromSystemServer();
1440         }
1441     }
1442 
1443     /**
1444      * Returns the names of all HealthConnect Backup files
1445      *
1446      * @hide
1447      */
getAllBackupFileNames(boolean forDeviceToDevice)1448     public Set<String> getAllBackupFileNames(boolean forDeviceToDevice) {
1449         try {
1450             return mService.getAllBackupFileNames(forDeviceToDevice).getFileNames();
1451         } catch (RemoteException e) {
1452             throw e.rethrowFromSystemServer();
1453         }
1454     }
1455 
1456     /**
1457      * Deletes all previously staged HealthConnect data from the disk. For testing purposes only.
1458      *
1459      * <p>This deletes only the staged data leaving any other Health Connect data untouched.
1460      *
1461      * @hide
1462      */
1463     @TestApi
1464     @UserHandleAware
deleteAllStagedRemoteData()1465     public void deleteAllStagedRemoteData() throws NullPointerException {
1466         try {
1467             mService.deleteAllStagedRemoteData(mContext.getUser());
1468         } catch (RemoteException e) {
1469             throw e.rethrowFromSystemServer();
1470         }
1471     }
1472 
1473     /**
1474      * Allows setting lower rate limits in tests.
1475      *
1476      * @hide
1477      */
1478     @TestApi
setLowerRateLimitsForTesting(boolean enabled)1479     public void setLowerRateLimitsForTesting(boolean enabled) throws NullPointerException {
1480         try {
1481             mService.setLowerRateLimitsForTesting(enabled);
1482         } catch (RemoteException e) {
1483             throw e.rethrowFromSystemServer();
1484         }
1485     }
1486 
1487     /**
1488      * Updates the download state of the Health Connect data.
1489      *
1490      * <p>The data should've been downloaded and the corresponding download states updated before
1491      * the app calls {@link #stageAllHealthConnectRemoteData}. Once {@link
1492      * #stageAllHealthConnectRemoteData} has been called the downloaded state becomes {@link
1493      * #DATA_DOWNLOAD_COMPLETE} and future attempts to update the download state are ignored.
1494      *
1495      * <p>The only valid order of state transition are:
1496      *
1497      * <ul>
1498      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_COMPLETE}
1499      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
1500      *       #DATA_DOWNLOAD_COMPLETE}
1501      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_FAILED}
1502      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
1503      *       #DATA_DOWNLOAD_FAILED}
1504      * </ul>
1505      *
1506      * <p>Note that it's okay if some states are missing in of the sequences above but the order has
1507      * to be one of the above.
1508      *
1509      * <p>Only one app will have the permission to call this API so it is assured that no one else
1510      * will be able to update this state.
1511      *
1512      * @param downloadState The download state which needs to be purely from {@link
1513      *     DataDownloadState}
1514      * @hide
1515      */
1516     @SystemApi
1517     @UserHandleAware
1518     @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA)
updateDataDownloadState(@ataDownloadState int downloadState)1519     public void updateDataDownloadState(@DataDownloadState int downloadState) {
1520         try {
1521             mService.updateDataDownloadState(downloadState);
1522         } catch (RemoteException e) {
1523             throw e.rethrowFromSystemServer();
1524         }
1525     }
1526 
1527     /**
1528      * Asynchronously returns the current UI state of Health Connect as it goes through the
1529      * Data-Migration process. In case there was an error reading the data on the disk the error
1530      * will be returned in the callback.
1531      *
1532      * <p>See also {@link HealthConnectMigrationUiState} object describing the HealthConnect UI
1533      * state.
1534      *
1535      * @param executor The {@link Executor} on which to invoke the callback.
1536      * @param callback The callback which will receive the current {@link
1537      *     HealthConnectMigrationUiState} or the {@link HealthConnectException}.
1538      * @hide
1539      */
1540     @UserHandleAware
1541     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
1542     @NonNull
getHealthConnectMigrationUiState( @onNull Executor executor, @NonNull OutcomeReceiver<HealthConnectMigrationUiState, HealthConnectException> callback)1543     public void getHealthConnectMigrationUiState(
1544             @NonNull Executor executor,
1545             @NonNull
1546                     OutcomeReceiver<HealthConnectMigrationUiState, HealthConnectException>
1547                             callback) {
1548         Objects.requireNonNull(executor);
1549         Objects.requireNonNull(callback);
1550 
1551         try {
1552             mService.getHealthConnectMigrationUiState(
1553                     new IGetHealthConnectMigrationUiStateCallback.Stub() {
1554                         @Override
1555                         public void onResult(HealthConnectMigrationUiState migrationUiState) {
1556                             Binder.clearCallingIdentity();
1557                             executor.execute(() -> callback.onResult(migrationUiState));
1558                         }
1559 
1560                         @Override
1561                         public void onError(HealthConnectExceptionParcel exception) {
1562                             Binder.clearCallingIdentity();
1563                             executor.execute(
1564                                     () -> callback.onError(exception.getHealthConnectException()));
1565                         }
1566                     });
1567         } catch (RemoteException e) {
1568             throw e.rethrowFromSystemServer();
1569         }
1570     }
1571 
1572     /**
1573      * Asynchronously returns the current state of the Health Connect data as it goes through the
1574      * Data-Restore and/or the Data-Migration process. In case there was an error reading the data
1575      * on the disk the error will be returned in the callback.
1576      *
1577      * <p>See also {@link HealthConnectDataState} object describing the HealthConnect state.
1578      *
1579      * @param executor The {@link Executor} on which to invoke the callback.
1580      * @param callback The callback which will receive the current {@link HealthConnectDataState} or
1581      *     the {@link HealthConnectException}.
1582      * @hide
1583      */
1584     @SystemApi
1585     @UserHandleAware
1586     @RequiresPermission(
1587             anyOf = {
1588                 MANAGE_HEALTH_DATA_PERMISSION,
1589                 Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA
1590             })
1591     @NonNull
getHealthConnectDataState( @onNull Executor executor, @NonNull OutcomeReceiver<HealthConnectDataState, HealthConnectException> callback)1592     public void getHealthConnectDataState(
1593             @NonNull Executor executor,
1594             @NonNull OutcomeReceiver<HealthConnectDataState, HealthConnectException> callback) {
1595         Objects.requireNonNull(executor);
1596         Objects.requireNonNull(callback);
1597         try {
1598             mService.getHealthConnectDataState(
1599                     new IGetHealthConnectDataStateCallback.Stub() {
1600                         @Override
1601                         public void onResult(HealthConnectDataState healthConnectDataState) {
1602                             Binder.clearCallingIdentity();
1603                             executor.execute(() -> callback.onResult(healthConnectDataState));
1604                         }
1605 
1606                         @Override
1607                         public void onError(HealthConnectExceptionParcel exception) {
1608                             Binder.clearCallingIdentity();
1609                             executor.execute(
1610                                     () -> callback.onError(exception.getHealthConnectException()));
1611                         }
1612                     });
1613         } catch (RemoteException e) {
1614             throw e.rethrowFromSystemServer();
1615         }
1616     }
1617 
1618     /**
1619      * Returns a list of unique dates for which the DB has at least one entry.
1620      *
1621      * @param recordTypes List of record types classes for which to get the activity dates.
1622      * @param executor Executor on which to invoke the callback.
1623      * @param callback Callback to receive result of performing this operation.
1624      * @throws java.lang.IllegalArgumentException If the record types list is empty.
1625      * @hide
1626      */
1627     @NonNull
1628     @SystemApi
1629     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryActivityDates( @onNull List<Class<? extends Record>> recordTypes, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<List<LocalDate>, HealthConnectException> callback)1630     public void queryActivityDates(
1631             @NonNull List<Class<? extends Record>> recordTypes,
1632             @NonNull @CallbackExecutor Executor executor,
1633             @NonNull OutcomeReceiver<List<LocalDate>, HealthConnectException> callback) {
1634         Objects.requireNonNull(executor);
1635         Objects.requireNonNull(callback);
1636         Objects.requireNonNull(recordTypes);
1637 
1638         if (recordTypes.isEmpty()) {
1639             throw new IllegalArgumentException("Record types list can not be empty");
1640         }
1641 
1642         try {
1643             mService.getActivityDates(
1644                     new ActivityDatesRequestParcel(recordTypes),
1645                     new IActivityDatesResponseCallback.Stub() {
1646                         @Override
1647                         public void onResult(ActivityDatesResponseParcel parcel) {
1648                             Binder.clearCallingIdentity();
1649                             executor.execute(() -> callback.onResult(parcel.getDates()));
1650                         }
1651 
1652                         @Override
1653                         public void onError(HealthConnectExceptionParcel exception) {
1654                             returnError(executor, exception, callback);
1655                         }
1656                     });
1657 
1658         } catch (RemoteException exception) {
1659             exception.rethrowFromSystemServer();
1660         }
1661     }
1662 
1663     /**
1664      * Marks the start of the migration and block API calls.
1665      *
1666      * @param executor Executor on which to invoke the callback.
1667      * @param callback Callback to receive result of performing this operation.
1668      * @hide
1669      */
1670     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1671     @SystemApi
startMigration( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1672     public void startMigration(
1673             @NonNull @CallbackExecutor Executor executor,
1674             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1675         Objects.requireNonNull(executor);
1676         Objects.requireNonNull(callback);
1677         try {
1678             mService.startMigration(
1679                     mContext.getPackageName(), wrapMigrationCallback(executor, callback));
1680         } catch (RemoteException e) {
1681             throw e.rethrowFromSystemServer();
1682         }
1683     }
1684 
1685     /**
1686      * Marks the end of the migration.
1687      *
1688      * @param executor Executor on which to invoke the callback.
1689      * @param callback Callback to receive result of performing this operation.
1690      * @hide
1691      */
1692     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1693     @SystemApi
finishMigration( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1694     public void finishMigration(
1695             @NonNull @CallbackExecutor Executor executor,
1696             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1697         Objects.requireNonNull(executor);
1698         Objects.requireNonNull(callback);
1699         try {
1700             mService.finishMigration(
1701                     mContext.getPackageName(), wrapMigrationCallback(executor, callback));
1702         } catch (RemoteException e) {
1703             throw e.rethrowFromSystemServer();
1704         }
1705     }
1706 
1707     /**
1708      * Writes data to the module database.
1709      *
1710      * @param entities List of {@link MigrationEntity} to migrate.
1711      * @param executor Executor on which to invoke the callback.
1712      * @param callback Callback to receive result of performing this operation.
1713      * @hide
1714      */
1715     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1716     @SystemApi
writeMigrationData( @onNull List<MigrationEntity> entities, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1717     public void writeMigrationData(
1718             @NonNull List<MigrationEntity> entities,
1719             @NonNull @CallbackExecutor Executor executor,
1720             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1721 
1722         Objects.requireNonNull(entities);
1723         Objects.requireNonNull(executor);
1724         Objects.requireNonNull(callback);
1725 
1726         try {
1727             mService.writeMigrationData(
1728                     mContext.getPackageName(),
1729                     new MigrationEntityParcel(entities),
1730                     wrapMigrationCallback(executor, callback));
1731         } catch (RemoteException e) {
1732             throw e.rethrowFromSystemServer();
1733         }
1734     }
1735 
1736     /**
1737      * Sets the minimum version on which the module will inform the migrator package of its
1738      * migration readiness.
1739      *
1740      * @param executor Executor on which to invoke the callback.
1741      * @param callback Callback to receive result of performing this operation.
1742      * @hide
1743      */
1744     @SystemApi
1745     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
insertMinDataMigrationSdkExtensionVersion( int requiredSdkExtension, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1746     public void insertMinDataMigrationSdkExtensionVersion(
1747             int requiredSdkExtension,
1748             @NonNull @CallbackExecutor Executor executor,
1749             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1750         Objects.requireNonNull(executor);
1751         Objects.requireNonNull(callback);
1752         try {
1753             mService.insertMinDataMigrationSdkExtensionVersion(
1754                     mContext.getPackageName(),
1755                     requiredSdkExtension,
1756                     wrapMigrationCallback(executor, callback));
1757 
1758         } catch (RemoteException e) {
1759             throw e.rethrowFromSystemServer();
1760         }
1761     }
1762 
1763     /** @hide */
1764     @Retention(RetentionPolicy.SOURCE)
1765     @IntDef({DATA_EXPORT_ERROR_UNKNOWN, DATA_EXPORT_ERROR_NONE, DATA_EXPORT_LOST_FILE_ACCESS})
1766     public @interface DataExportError {}
1767 
1768     /**
1769      * Configures the settings for the scheduled export of Health Connect data.
1770      *
1771      * @param settings Settings to use for the scheduled export. Use null to clear the settings.
1772      * @throws RuntimeException for internal errors
1773      * @hide
1774      */
1775     @SuppressWarnings("NullAway") // TODO: b/178748627 - fix this suppression.
1776     @WorkerThread
1777     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
configureScheduledExport(@ullable ScheduledExportSettings settings)1778     public void configureScheduledExport(@Nullable ScheduledExportSettings settings) {
1779         try {
1780             mService.configureScheduledExport(settings, mContext.getUser());
1781         } catch (RemoteException e) {
1782             e.rethrowFromSystemServer();
1783         }
1784     }
1785 
1786     /**
1787      * Queries the status of a scheduled export.
1788      *
1789      * @throws RuntimeException for internal errors
1790      * @hide
1791      */
1792     @FlaggedApi(FLAG_EXPORT_IMPORT)
1793     @WorkerThread
1794     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getScheduledExportStatus( @onNull Executor executor, @NonNull OutcomeReceiver<ScheduledExportStatus, HealthConnectException> callback)1795     public void getScheduledExportStatus(
1796             @NonNull Executor executor,
1797             @NonNull OutcomeReceiver<ScheduledExportStatus, HealthConnectException> callback) {
1798         Objects.requireNonNull(executor);
1799         Objects.requireNonNull(callback);
1800 
1801         try {
1802             mService.getScheduledExportStatus(
1803                     mContext.getUser(),
1804                     new IScheduledExportStatusCallback.Stub() {
1805                         @Override
1806                         public void onResult(ScheduledExportStatus status) {
1807                             Binder.clearCallingIdentity();
1808                             executor.execute(() -> callback.onResult(status));
1809                         }
1810 
1811                         @Override
1812                         public void onError(HealthConnectExceptionParcel exception) {
1813                             returnError(executor, exception, callback);
1814                         }
1815                     });
1816         } catch (RemoteException e) {
1817             e.rethrowFromSystemServer();
1818         }
1819     }
1820 
1821     /**
1822      * Queries the status of a data import.
1823      *
1824      * @throws RuntimeException for internal errors
1825      * @hide
1826      */
1827     @FlaggedApi(FLAG_EXPORT_IMPORT)
1828     @WorkerThread
1829     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getImportStatus( @onNull Executor executor, @NonNull OutcomeReceiver<ImportStatus, HealthConnectException> callback)1830     public void getImportStatus(
1831             @NonNull Executor executor,
1832             @NonNull OutcomeReceiver<ImportStatus, HealthConnectException> callback) {
1833         Objects.requireNonNull(executor);
1834         Objects.requireNonNull(callback);
1835 
1836         try {
1837             mService.getImportStatus(
1838                     mContext.getUser(),
1839                     new IImportStatusCallback.Stub() {
1840                         @Override
1841                         public void onResult(ImportStatus status) {
1842                             Binder.clearCallingIdentity();
1843                             executor.execute(() -> callback.onResult(status));
1844                         }
1845 
1846                         @Override
1847                         public void onError(HealthConnectExceptionParcel exception) {
1848                             returnError(executor, exception, callback);
1849                         }
1850                     });
1851         } catch (RemoteException e) {
1852             e.rethrowFromSystemServer();
1853         }
1854     }
1855 
1856     /**
1857      * Queries the status of a data import.
1858      *
1859      * @throws RuntimeException for internal errors
1860      * @hide
1861      */
1862     @FlaggedApi(FLAG_EXPORT_IMPORT)
1863     @WorkerThread
1864     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
runImport(@onNull Uri file)1865     public void runImport(@NonNull Uri file) {
1866         Objects.requireNonNull(file);
1867         try {
1868             mService.runImport(mContext.getUser(), file);
1869         } catch (RemoteException e) {
1870             e.rethrowFromSystemServer();
1871         }
1872     }
1873 
1874     /**
1875      * Returns currently set period between scheduled exports for this user.
1876      *
1877      * <p>If you are calling this function for the first time after a user unlock, this might take
1878      * some time so consider calling this on a thread.
1879      *
1880      * @return Period between scheduled exports in days, 0 is returned if period between scheduled
1881      *     exports is not set.
1882      * @throws RuntimeException for internal errors
1883      * @hide
1884      */
1885     @WorkerThread
1886     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
1887     @IntRange(from = 0, to = 30)
getScheduledExportPeriodInDays()1888     public int getScheduledExportPeriodInDays() {
1889         try {
1890             return mService.getScheduledExportPeriodInDays(mContext.getUser());
1891         } catch (RemoteException e) {
1892             throw e.rethrowFromSystemServer();
1893         }
1894     }
1895 
1896     /**
1897      * Queries the document providers available to be used for export/import.
1898      *
1899      * @throws RuntimeException for internal errors
1900      * @hide
1901      */
1902     @FlaggedApi(FLAG_EXPORT_IMPORT)
1903     @WorkerThread
1904     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryDocumentProviders( @onNull Executor executor, @NonNull OutcomeReceiver<List<ExportImportDocumentProvider>, HealthConnectException> callback)1905     public void queryDocumentProviders(
1906             @NonNull Executor executor,
1907             @NonNull
1908                     OutcomeReceiver<List<ExportImportDocumentProvider>, HealthConnectException>
1909                             callback) {
1910         Objects.requireNonNull(executor);
1911         Objects.requireNonNull(callback);
1912 
1913         try {
1914             mService.queryDocumentProviders(
1915                     mContext.getUser(),
1916                     new IQueryDocumentProvidersCallback.Stub() {
1917                         @Override
1918                         public void onResult(List<ExportImportDocumentProvider> providers) {
1919                             Binder.clearCallingIdentity();
1920                             executor.execute(() -> callback.onResult(providers));
1921                         }
1922 
1923                         @Override
1924                         public void onError(HealthConnectExceptionParcel exception) {
1925                             returnError(executor, exception, callback);
1926                         }
1927                     });
1928         } catch (RemoteException e) {
1929             e.rethrowFromSystemServer();
1930         }
1931     }
1932 
1933     @SuppressWarnings("unchecked")
getReadCallback( @onNull Executor executor, @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback)1934     private <T extends Record> IReadRecordsResponseCallback.Stub getReadCallback(
1935             @NonNull Executor executor,
1936             @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
1937         return new IReadRecordsResponseCallback.Stub() {
1938             @Override
1939             public void onResult(ReadRecordsResponseParcel parcel) {
1940                 Binder.clearCallingIdentity();
1941                 try {
1942                     List<T> externalRecords =
1943                             (List<T>)
1944                                     mInternalExternalRecordConverter.getExternalRecords(
1945                                             parcel.getRecordsParcel().getRecords());
1946                     executor.execute(
1947                             () ->
1948                                     callback.onResult(
1949                                             new ReadRecordsResponse<>(
1950                                                     externalRecords, parcel.getPageToken())));
1951                 } catch (ClassCastException castException) {
1952                     HealthConnectException healthConnectException =
1953                             new HealthConnectException(
1954                                     HealthConnectException.ERROR_INTERNAL,
1955                                     castException.getMessage());
1956                     returnError(
1957                             executor,
1958                             new HealthConnectExceptionParcel(healthConnectException),
1959                             callback);
1960                 }
1961             }
1962 
1963             @Override
1964             public void onError(HealthConnectExceptionParcel exception) {
1965                 returnError(executor, exception, callback);
1966             }
1967         };
1968     }
1969 
1970     private List<Record> toExternalRecordsWithUuids(
1971             List<RecordInternal<?>> recordInternals, List<String> uuids) {
1972         int i = 0;
1973         List<Record> records = new ArrayList<>();
1974 
1975         for (RecordInternal recordInternal : recordInternals) {
1976             recordInternal.setUuid(uuids.get(i++));
1977             records.add(recordInternal.toExternalRecord());
1978         }
1979 
1980         return records;
1981     }
1982 
1983     private static <RES, ERR extends Throwable> void returnResult(
1984             Executor executor, @Nullable RES result, OutcomeReceiver<RES, ERR> callback) {
1985         Binder.clearCallingIdentity();
1986         executor.execute(() -> callback.onResult(result));
1987     }
1988 
1989     private void returnError(
1990             Executor executor,
1991             HealthConnectExceptionParcel exception,
1992             OutcomeReceiver<?, HealthConnectException> callback) {
1993         Binder.clearCallingIdentity();
1994         executor.execute(() -> callback.onError(exception.getHealthConnectException()));
1995     }
1996 
1997     /** @hide */
1998     @Retention(RetentionPolicy.SOURCE)
1999     @IntDef({
2000         DATA_DOWNLOAD_STATE_UNKNOWN,
2001         DATA_DOWNLOAD_STARTED,
2002         DATA_DOWNLOAD_RETRY,
2003         DATA_DOWNLOAD_FAILED,
2004         DATA_DOWNLOAD_COMPLETE
2005     })
2006     public @interface DataDownloadState {}
2007 
2008     /**
2009      * Returns {@code true} if the given permission protects access to health connect data.
2010      *
2011      * @hide
2012      */
2013     @SystemApi
2014     public static boolean isHealthPermission(
2015             @NonNull Context context, @NonNull final String permission) {
2016         if (!permission.startsWith(HEALTH_PERMISSION_PREFIX)) {
2017             return false;
2018         }
2019         return getHealthPermissions(context).contains(permission);
2020     }
2021 
2022     /**
2023      * Returns an <b>immutable</b> set of health permissions defined within the module and belonging
2024      * to {@link android.health.connect.HealthPermissions#HEALTH_PERMISSION_GROUP}.
2025      *
2026      * <p><b>Note:</b> If we, for some reason, fail to retrieve these, we return an empty set rather
2027      * than crashing the device. This means the health permissions infra will be inactive.
2028      *
2029      * @hide
2030      */
2031     @NonNull
2032     @SystemApi
2033     public static Set<String> getHealthPermissions(@NonNull Context context) {
2034         if (sHealthPermissions != null) {
2035             return sHealthPermissions;
2036         }
2037 
2038         PackageInfo packageInfo;
2039         try {
2040             final PackageManager pm = context.getApplicationContext().getPackageManager();
2041             final PermissionGroupInfo permGroupInfo =
2042                     pm.getPermissionGroupInfo(
2043                             android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP,
2044                             /* flags= */ 0);
2045             packageInfo =
2046                     pm.getPackageInfo(
2047                             permGroupInfo.packageName,
2048                             PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
2049         } catch (PackageManager.NameNotFoundException ex) {
2050             Log.e(TAG, "Health permission group or HC package not found", ex);
2051             sHealthPermissions = Collections.emptySet();
2052             return sHealthPermissions;
2053         }
2054 
2055         Set<String> permissions = new HashSet<>();
2056         for (PermissionInfo perm : packageInfo.permissions) {
2057             if (android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP.equals(
2058                     perm.group)) {
2059                 permissions.add(perm.name);
2060             }
2061         }
2062         sHealthPermissions = Collections.unmodifiableSet(permissions);
2063         return sHealthPermissions;
2064     }
2065 
2066     @NonNull
2067     private static IMigrationCallback wrapMigrationCallback(
2068             @NonNull @CallbackExecutor Executor executor,
2069             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
2070         return new IMigrationCallback.Stub() {
2071             @Override
2072             public void onSuccess() {
2073                 Binder.clearCallingIdentity();
2074                 executor.execute(() -> callback.onResult(null));
2075             }
2076 
2077             @Override
2078             public void onError(MigrationException exception) {
2079                 Binder.clearCallingIdentity();
2080                 executor.execute(() -> callback.onError(exception));
2081             }
2082         };
2083     }
2084 
2085     /**
2086      * Inserts or updates a list of {@link MedicalResource}s into the HealthConnect database.
2087      *
2088      * <p>The uniqueness is calculated comparing the combination of {@link
2089      * UpsertMedicalResourceRequest#getDataSourceId() data source id}, FHIR resource type and FHIR
2090      * resource id extracted from the provided {@link MedicalResource} data. If there is no match,
2091      * then a new {@link MedicalResource} is inserted, otherwise the existing one is updated.
2092      *
2093      * @hide
2094      */
2095     public void upsertMedicalResources(
2096             @NonNull List<UpsertMedicalResourceRequest> requests,
2097             @NonNull @CallbackExecutor Executor executor,
2098             @NonNull OutcomeReceiver<List<MedicalResource>, HealthConnectException> callback) {
2099         Objects.requireNonNull(requests);
2100         Objects.requireNonNull(executor);
2101         Objects.requireNonNull(callback);
2102 
2103         throw new UnsupportedOperationException("Not implemented");
2104     }
2105 
2106     /**
2107      * Reads {@link MedicalResource}s based on a list of {@link MedicalIdFilter}s.
2108      *
2109      * <p>Number of resources returned by this API will depend based on below factors:
2110      *
2111      * <ul>
2112      *   <li>When an app with read permissions allowed for the requested IDs but without the {@link
2113      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} calls the API
2114      *       from background then it will be able to read only its own inserted medical resources
2115      *       and will not get medical resources inserted by other apps. This may be less than the
2116      *       requested size.
2117      *   <li>When an app with read permissions allowed for the requested IDs and with the {@link
2118      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} calls the API
2119      *       from background then it will be able to read the medical resources it has read
2120      *       permissions for. This has the same size the app can read from foreground.
2121      *   <li>When an app with all read permissions allowed for the requested IDs calls the API from
2122      *       foreground then it will be able to read all the corresponding medical resources.
2123      *   <li>When an app with less read permissions allowed to cover all the requested IDs calls the
2124      *       API from foreground then it will be able to read only the medical resources it has read
2125      *       permissions for. This may be less than the requested size.
2126      *   <li>App with only write permission but no read permission allowed will be able to read only
2127      *       its own inserted medical resources both when in foreground or background. This may be
2128      *       less than the requested size.
2129      *   <li>An app without both read and write permissions will not be able to read any medical
2130      *       resources and the API will throw Security Exception.
2131      * </ul>
2132      *
2133      * @param ids Identifiers on which to perform read operation.
2134      * @param executor Executor on which to invoke the callback.
2135      * @param callback Callback to receive result of performing this operation.
2136      * @throws IllegalArgumentException if {@code ids} is empty or its size is more than 5000.
2137      */
2138     // (TODO: b/343455447): Replace MedicalIdFilter to use MedicalResourceId.
2139     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2140     public void readMedicalResources(
2141             @NonNull List<MedicalIdFilter> ids,
2142             @NonNull Executor executor,
2143             @NonNull OutcomeReceiver<List<MedicalResource>, HealthConnectException> callback) {
2144         Objects.requireNonNull(ids);
2145         Objects.requireNonNull(executor);
2146         Objects.requireNonNull(callback);
2147 
2148         if (ids.isEmpty()) {
2149             returnResult(executor, List.of(), callback);
2150             return;
2151         }
2152 
2153         if (ids.size() >= MAXIMUM_PAGE_SIZE) {
2154             throw new IllegalArgumentException("Maximum allowed pageSize is " + MAXIMUM_PAGE_SIZE);
2155         }
2156 
2157         try {
2158             mService.readMedicalResources(
2159                     mContext.getAttributionSource(),
2160                     new MedicalIdFiltersParcel(ids),
2161                     new IReadMedicalResourcesResponseCallback.Stub() {
2162                         @Override
2163                         public void onResult(ReadMedicalResourcesResponse parcel) {
2164                             returnResult(executor, parcel.getMedicalResources(), callback);
2165                         }
2166 
2167                         @Override
2168                         public void onError(HealthConnectExceptionParcel exception) {
2169                             returnError(executor, exception, callback);
2170                         }
2171                     });
2172         } catch (RemoteException e) {
2173             throw e.rethrowFromSystemServer();
2174         }
2175     }
2176 
2177     /**
2178      * Reads {@link MedicalResource}s based on given filters in {@link ReadMedicalResourcesRequest}.
2179      *
2180      * <p>Number of resources returned by this API will depend based on below factors:
2181      *
2182      * <ul>
2183      *   <li>When an app with read permissions allowed for the requested filters but without the
2184      *       {@link android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} calls
2185      *       the API from background then it will be able to read only its own inserted medical
2186      *       resources and will not get medical resources inserted by other apps. This may be less
2187      *       than the size the app can read from foreground.
2188      *   <li>When an app with read permissions allowed for the requested filters and with the {@link
2189      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} calls the API
2190      *       from background then it will be able to read all the corresponding medical resources.
2191      *       This has the same size the app can read from foreground.
2192      *   <li>When an app with the read permissions allowed for the requested filters calls the API
2193      *       from foreground then it will be able to read all the corresponding medical resources.
2194      *   <li>App with only write permission but no read permission allowed will be able to read only
2195      *       its own inserted medical resources both when in foreground or background.
2196      *   <li>An app without both read and write permissions will not be able to read any medical
2197      *       resources and the API will throw Security Exception.
2198      * </ul>
2199      *
2200      * @param request The read request.
2201      * @param executor Executor on which to invoke the callback.
2202      * @param callback Callback to receive result of performing this operation.
2203      * @throws IllegalArgumentException if request page size set is less than 1 or more than 5000 in
2204      *     {@link ReadMedicalResourcesRequest}.
2205      */
2206     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2207     public void readMedicalResources(
2208             @NonNull ReadMedicalResourcesRequest request,
2209             @NonNull Executor executor,
2210             @NonNull
2211                     OutcomeReceiver<ReadMedicalResourcesResponse, HealthConnectException>
2212                             callback) {
2213         Objects.requireNonNull(request);
2214         Objects.requireNonNull(executor);
2215         Objects.requireNonNull(callback);
2216 
2217         throw new UnsupportedOperationException("Not implemented");
2218     }
2219 
2220     /**
2221      * Deletes a list of medical resources by id. Ids that don't exist will be ignored.
2222      *
2223      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none.
2224      *
2225      * @param ids The ids to delete.
2226      * @param executor Executor on which to invoke the callback.
2227      * @param callback Callback to receive result of performing this operation.
2228      * @hide
2229      */
2230     // TODO: b/338035191 - Make this flagged Api and add CTS tests.
2231     public void deleteMedicalResources(
2232             @NonNull List<MedicalIdFilter> ids,
2233             @NonNull Executor executor,
2234             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
2235         throw new UnsupportedOperationException("Not implemented");
2236     }
2237 
2238     /**
2239      * Creates a {@link MedicalDataSource} in HealthConnect based on the {@link
2240      * CreateMedicalDataSourceRequest} request values.
2241      *
2242      * <p>A {@link MedicalDataSource} needs to be created before any {@link MedicalResource}s for
2243      * that source can be inserted.
2244      *
2245      * <p>The following rules apply to {@link MedicalDataSource} creation.
2246      *
2247      * <ul>
2248      *   <li>Only apps that have the android.health.connect.HealthPermissions#WRITE_MEDICAL_DATA are
2249      *       allowed to create data sources.
2250      *   <li>The {@link CreateMedicalDataSourceRequest.Builder#setFhirBaseUri} must be unique across
2251      *       all medical data sources created by an app. The FHIR base uri cannot be updated after
2252      *       creating the data source.
2253      * </ul>
2254      *
2255      * @param request Creation request.
2256      * @param executor Executor on which to invoke the callback.
2257      * @param callback Callback to receive result of performing this operation.
2258      */
2259     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2260     public void createMedicalDataSource(
2261             @NonNull CreateMedicalDataSourceRequest request,
2262             @NonNull Executor executor,
2263             @NonNull OutcomeReceiver<MedicalDataSource, HealthConnectException> callback) {
2264         Objects.requireNonNull(request);
2265         Objects.requireNonNull(executor);
2266         Objects.requireNonNull(callback);
2267 
2268         throw new UnsupportedOperationException("Not implemented");
2269     }
2270 
2271     /**
2272      * Returns {@link MedicalDataSource}s for the provided list of {@link MedicalDataSource} ids.
2273      *
2274      * <p>The returned list of data sources will be in the same order as the {@code ids}.
2275      *
2276      * <p>Number of data sources returned by this API will depend based on below factors:
2277      *
2278      * <ul>
2279      *   <li>If an empty list of {@code ids} is provided, no data sources will be returned.
2280      *   <li>When an app with any read permission for medical data but without the {@link
2281      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} calls the API
2282      *       from the background then it will be able to read only its own inserted medical data
2283      *       sources and will not get medical data sources inserted by other apps. This may be less
2284      *       than the requested size.
2285      *   <li>When an app with any read permission for medical data and with the {@link
2286      *       android.health.connect.HealthPermissions#READ_HEALTH_DATA_IN_BACKGROUND} calls the API
2287      *       from the background then it will be able to read all medical data sources.
2288      *   <li>When an app with any read permission for medical data calls the API from the foreground
2289      *       then it will be able to read all medical data sources.
2290      *   <li>App with only write permission but no read permission allowed will be able to read only
2291      *       its own inserted medical data sources both when in foreground or background. This may
2292      *       be less than the requested size.
2293      *   <li>An app without read or write permissions will not be able to read any medical data
2294      *       sources and the API will throw Security Exception.
2295      * </ul>
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      */
2301     @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD)
2302     public void getMedicalDataSources(
2303             @NonNull List<String> ids,
2304             @NonNull Executor executor,
2305             @NonNull OutcomeReceiver<List<MedicalDataSource>, HealthConnectException> callback) {
2306         Objects.requireNonNull(ids);
2307         Objects.requireNonNull(executor);
2308         Objects.requireNonNull(callback);
2309 
2310         if (ids.isEmpty()) {
2311             returnResult(executor, List.of(), callback);
2312             return;
2313         }
2314 
2315         throw new UnsupportedOperationException("Not implemented");
2316     }
2317 }
2318