• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.health.connect;
18 
19 import static android.health.connect.Constants.DEFAULT_LONG;
20 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
21 import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
22 
23 import android.Manifest;
24 import android.annotation.CallbackExecutor;
25 import android.annotation.IntDef;
26 import android.annotation.IntRange;
27 import android.annotation.NonNull;
28 import android.annotation.Nullable;
29 import android.annotation.RequiresPermission;
30 import android.annotation.SdkConstant;
31 import android.annotation.SystemApi;
32 import android.annotation.SystemService;
33 import android.annotation.TestApi;
34 import android.annotation.UserHandleAware;
35 import android.content.Context;
36 import android.content.pm.PackageInfo;
37 import android.content.pm.PackageManager;
38 import android.content.pm.PermissionGroupInfo;
39 import android.content.pm.PermissionInfo;
40 import android.health.connect.accesslog.AccessLog;
41 import android.health.connect.accesslog.AccessLogsResponseParcel;
42 import android.health.connect.aidl.ActivityDatesRequestParcel;
43 import android.health.connect.aidl.ActivityDatesResponseParcel;
44 import android.health.connect.aidl.AggregateDataRequestParcel;
45 import android.health.connect.aidl.AggregateDataResponseParcel;
46 import android.health.connect.aidl.ApplicationInfoResponseParcel;
47 import android.health.connect.aidl.DeleteUsingFiltersRequestParcel;
48 import android.health.connect.aidl.GetPriorityResponseParcel;
49 import android.health.connect.aidl.HealthConnectExceptionParcel;
50 import android.health.connect.aidl.IAccessLogsResponseCallback;
51 import android.health.connect.aidl.IActivityDatesResponseCallback;
52 import android.health.connect.aidl.IAggregateRecordsResponseCallback;
53 import android.health.connect.aidl.IApplicationInfoResponseCallback;
54 import android.health.connect.aidl.IChangeLogsResponseCallback;
55 import android.health.connect.aidl.IDataStagingFinishedCallback;
56 import android.health.connect.aidl.IEmptyResponseCallback;
57 import android.health.connect.aidl.IGetChangeLogTokenCallback;
58 import android.health.connect.aidl.IGetHealthConnectDataStateCallback;
59 import android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback;
60 import android.health.connect.aidl.IGetPriorityResponseCallback;
61 import android.health.connect.aidl.IHealthConnectService;
62 import android.health.connect.aidl.IInsertRecordsResponseCallback;
63 import android.health.connect.aidl.IMigrationCallback;
64 import android.health.connect.aidl.IReadRecordsResponseCallback;
65 import android.health.connect.aidl.IRecordTypeInfoResponseCallback;
66 import android.health.connect.aidl.InsertRecordsResponseParcel;
67 import android.health.connect.aidl.ReadRecordsResponseParcel;
68 import android.health.connect.aidl.RecordIdFiltersParcel;
69 import android.health.connect.aidl.RecordTypeInfoResponseParcel;
70 import android.health.connect.aidl.RecordsParcel;
71 import android.health.connect.aidl.UpdatePriorityRequestParcel;
72 import android.health.connect.changelog.ChangeLogTokenRequest;
73 import android.health.connect.changelog.ChangeLogTokenResponse;
74 import android.health.connect.changelog.ChangeLogsRequest;
75 import android.health.connect.changelog.ChangeLogsResponse;
76 import android.health.connect.datatypes.AggregationType;
77 import android.health.connect.datatypes.DataOrigin;
78 import android.health.connect.datatypes.Record;
79 import android.health.connect.internal.datatypes.RecordInternal;
80 import android.health.connect.internal.datatypes.utils.InternalExternalRecordConverter;
81 import android.health.connect.migration.HealthConnectMigrationUiState;
82 import android.health.connect.migration.MigrationEntity;
83 import android.health.connect.migration.MigrationEntityParcel;
84 import android.health.connect.migration.MigrationException;
85 import android.health.connect.restore.StageRemoteDataException;
86 import android.health.connect.restore.StageRemoteDataRequest;
87 import android.os.Binder;
88 import android.os.OutcomeReceiver;
89 import android.os.ParcelFileDescriptor;
90 import android.os.RemoteException;
91 import android.util.Log;
92 
93 import java.lang.annotation.Retention;
94 import java.lang.annotation.RetentionPolicy;
95 import java.time.Duration;
96 import java.time.Instant;
97 import java.time.LocalDate;
98 import java.time.Period;
99 import java.time.ZoneOffset;
100 import java.util.ArrayList;
101 import java.util.Collections;
102 import java.util.HashSet;
103 import java.util.List;
104 import java.util.Map;
105 import java.util.Objects;
106 import java.util.Set;
107 import java.util.concurrent.Executor;
108 import java.util.stream.Collectors;
109 
110 /**
111  * This class provides APIs to interact with the centralized HealthConnect storage maintained by the
112  * system.
113  *
114  * <p>HealthConnect is an offline, on-device storage that unifies data from multiple devices and
115  * apps into an ecosystem featuring.
116  *
117  * <ul>
118  *   <li>APIs to insert data of various types into the system.
119  * </ul>
120  *
121  * <p>The basic unit of data in HealthConnect is represented as a {@link Record} object, which is
122  * the base class for all the other data types such as {@link
123  * android.health.connect.datatypes.StepsRecord}.
124  */
125 @SystemService(Context.HEALTHCONNECT_SERVICE)
126 public class HealthConnectManager {
127     /**
128      * Used in conjunction with {@link android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} to
129      * launch UI to show an app’s health permission rationale/data policy.
130      *
131      * <p><b>Note:</b> Used by apps to define an intent filter in conjunction with {@link
132      * android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} that the HC UI can link out to.
133      */
134     // We use intent.category prefix to be compatible with HealthPermissions strings definitions.
135     @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY)
136     public static final String CATEGORY_HEALTH_PERMISSIONS =
137             "android.intent.category.HEALTH_PERMISSIONS";
138 
139     /**
140      * Activity action: Launch UI to manage (e.g. grant/revoke) health permissions.
141      *
142      * <p>Shows a list of apps which request at least one permission of the Health permission group.
143      *
144      * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the
145      * app requesting the action. Optional: Adding package name extras launches a UI to manager
146      * (e.g. grant/revoke) for this app.
147      */
148     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
149     public static final String ACTION_MANAGE_HEALTH_PERMISSIONS =
150             "android.health.connect.action.MANAGE_HEALTH_PERMISSIONS";
151 
152     /**
153      * Activity action: Launch UI to share the route associated with an exercise session.
154      *
155      * <p>Input: caller must provide `String` extra EXTRA_SESSION_ID
156      *
157      * <p>Result will be delivered via [Activity.onActivityResult] with `ExerciseRoute`
158      * EXTRA_EXERCISE_ROUTE.
159      */
160     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
161     public static final String ACTION_REQUEST_EXERCISE_ROUTE =
162             "android.health.connect.action.REQUEST_EXERCISE_ROUTE";
163 
164     /**
165      * A string ID of a session to be used with {@link #ACTION_REQUEST_EXERCISE_ROUTE}.
166      *
167      * <p>This is used to specify route of which exercise session we want to request.
168      */
169     public static final String EXTRA_SESSION_ID = "android.health.connect.extra.SESSION_ID";
170 
171     /**
172      * An exercise route requested via {@link #ACTION_REQUEST_EXERCISE_ROUTE}.
173      *
174      * <p>This is returned for a successful request to access a route associated with an exercise
175      * session.
176      */
177     public static final String EXTRA_EXERCISE_ROUTE = "android.health.connect.extra.EXERCISE_ROUTE";
178 
179     /**
180      * Activity action: Launch UI to show and manage (e.g. grant/revoke) health permissions.
181      *
182      * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the
183      * app requesting the action must be present. An app can open only its own page.
184      *
185      * <p>Input: caller must provide `String[]` extra [EXTRA_PERMISSIONS]
186      *
187      * <p>Result will be delivered via [Activity.onActivityResult] with `String[]`
188      * [EXTRA_PERMISSIONS] and `int[]` [EXTRA_PERMISSION_GRANT_RESULTS], similar to
189      * [Activity.onRequestPermissionsResult]
190      *
191      * @hide
192      */
193     @SystemApi
194     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
195     public static final String ACTION_REQUEST_HEALTH_PERMISSIONS =
196             "android.health.connect.action.REQUEST_HEALTH_PERMISSIONS";
197 
198     /**
199      * Activity action: Launch UI to health connect home settings screen.
200      *
201      * <p>shows a list of recent apps that accessed (e.g. read/write) health data and allows the
202      * user to access health permissions and health data.
203      *
204      * @hide
205      */
206     @SystemApi
207     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
208     public static final String ACTION_HEALTH_HOME_SETTINGS =
209             "android.health.connect.action.HEALTH_HOME_SETTINGS";
210 
211     /**
212      * Activity action: Launch UI to show and manage (e.g. delete/export) health data.
213      *
214      * <p>shows a list of health data categories and actions to manage (e.g. delete/export) health
215      * data.
216      *
217      * @hide
218      */
219     @SystemApi
220     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
221     public static final String ACTION_MANAGE_HEALTH_DATA =
222             "android.health.connect.action.MANAGE_HEALTH_DATA";
223 
224     /**
225      * Activity action: Display information regarding migration - e.g. asking the user to take some
226      * action (e.g. update the system) so that migration can take place.
227      *
228      * <p><b>Note:</b> Callers of the migration APIs must handle this intent.
229      *
230      * @hide
231      */
232     @SystemApi
233     @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
234     public static final String ACTION_SHOW_MIGRATION_INFO =
235             "android.health.connect.action.SHOW_MIGRATION_INFO";
236 
237     /**
238      * Broadcast Action: Health Connect is ready to accept migrated data.
239      *
240      * <p class="note">This broadcast is explicitly sent to Health Connect migration aware
241      * applications to prompt them to start/continue HC data migration. Migration aware applications
242      * are those that both hold {@code android.permission.MIGRATE_HEALTH_CONNECT_DATA} and handle
243      * {@code android.health.connect.action.SHOW_MIGRATION_INFO}.
244      *
245      * <p class="note">This is a protected intent that can only be sent by the system.
246      *
247      * @hide
248      */
249     @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
250     @SystemApi
251     public static final String ACTION_HEALTH_CONNECT_MIGRATION_READY =
252             "android.health.connect.action.HEALTH_CONNECT_MIGRATION_READY";
253     /**
254      * Unknown download state considered to be the default download state.
255      *
256      * <p>See also {@link #updateDataDownloadState}
257      *
258      * @hide
259      */
260     @SystemApi public static final int DATA_DOWNLOAD_STATE_UNKNOWN = 0;
261     /**
262      * Indicates that the download has started.
263      *
264      * <p>See also {@link #updateDataDownloadState}
265      *
266      * @hide
267      */
268     @SystemApi public static final int DATA_DOWNLOAD_STARTED = 1;
269     /**
270      * Indicates that the download is being retried.
271      *
272      * <p>See also {@link #updateDataDownloadState}
273      *
274      * @hide
275      */
276     @SystemApi public static final int DATA_DOWNLOAD_RETRY = 2;
277     /**
278      * Indicates that the download has failed.
279      *
280      * <p>See also {@link #updateDataDownloadState}
281      *
282      * @hide
283      */
284     @SystemApi public static final int DATA_DOWNLOAD_FAILED = 3;
285     /**
286      * Indicates that the download has completed.
287      *
288      * <p>See also {@link HealthConnectManager#updateDataDownloadState}
289      *
290      * @hide
291      */
292     @SystemApi public static final int DATA_DOWNLOAD_COMPLETE = 4;
293 
294     private static final String TAG = "HealthConnectManager";
295     private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health.";
296     private static volatile Set<String> sHealthPermissions;
297     private final Context mContext;
298     private final IHealthConnectService mService;
299     private final InternalExternalRecordConverter mInternalExternalRecordConverter;
300 
301     /** @hide */
HealthConnectManager(@onNull Context context, @NonNull IHealthConnectService service)302     HealthConnectManager(@NonNull Context context, @NonNull IHealthConnectService service) {
303         mContext = context;
304         mService = service;
305         mInternalExternalRecordConverter = InternalExternalRecordConverter.getInstance();
306     }
307 
308     /**
309      * Grant a runtime permission to an application which the application does not already have. The
310      * permission must have been requested by the application. If the application is not allowed to
311      * hold the permission, a {@link java.lang.SecurityException} is thrown. If the package or
312      * permission is invalid, a {@link java.lang.IllegalArgumentException} is thrown.
313      *
314      * @hide
315      */
316     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
317     @UserHandleAware
grantHealthPermission(@onNull String packageName, @NonNull String permissionName)318     public void grantHealthPermission(@NonNull String packageName, @NonNull String permissionName) {
319         try {
320             mService.grantHealthPermission(packageName, permissionName, mContext.getUser());
321         } catch (RemoteException e) {
322             throw e.rethrowFromSystemServer();
323         }
324     }
325 
326     /**
327      * Revoke a health permission that was previously granted by {@link
328      * #grantHealthPermission(String, String)} The permission must have been requested by the
329      * application. If the application is not allowed to hold the permission, a {@link
330      * java.lang.SecurityException} is thrown. If the package or permission is invalid, a {@link
331      * java.lang.IllegalArgumentException} is thrown.
332      *
333      * @hide
334      */
335     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
336     @UserHandleAware
revokeHealthPermission( @onNull String packageName, @NonNull String permissionName, @Nullable String reason)337     public void revokeHealthPermission(
338             @NonNull String packageName, @NonNull String permissionName, @Nullable String reason) {
339         try {
340             mService.revokeHealthPermission(
341                     packageName, permissionName, reason, mContext.getUser());
342         } catch (RemoteException e) {
343             throw e.rethrowFromSystemServer();
344         }
345     }
346 
347     /**
348      * Revokes all health permissions that were previously granted by {@link
349      * #grantHealthPermission(String, String)} If the package is invalid, a {@link
350      * java.lang.IllegalArgumentException} is thrown.
351      *
352      * @hide
353      */
354     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
355     @UserHandleAware
revokeAllHealthPermissions(@onNull String packageName, @Nullable String reason)356     public void revokeAllHealthPermissions(@NonNull String packageName, @Nullable String reason) {
357         try {
358             mService.revokeAllHealthPermissions(packageName, reason, mContext.getUser());
359         } catch (RemoteException e) {
360             throw e.rethrowFromSystemServer();
361         }
362     }
363 
364     /**
365      * Returns a list of health permissions that were previously granted by {@link
366      * #grantHealthPermission(String, String)}.
367      *
368      * @hide
369      */
370     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
371     @UserHandleAware
getGrantedHealthPermissions(@onNull String packageName)372     public List<String> getGrantedHealthPermissions(@NonNull String packageName) {
373         try {
374             return mService.getGrantedHealthPermissions(packageName, mContext.getUser());
375         } catch (RemoteException e) {
376             throw e.rethrowFromSystemServer();
377         }
378     }
379 
380     /**
381      * Returns the date from which an app have access to the historical health data. Returns null if
382      * the package doesn't have historical access date.
383      *
384      * @hide
385      */
386     @RequiresPermission(HealthPermissions.MANAGE_HEALTH_PERMISSIONS)
387     @UserHandleAware
388     @Nullable
getHealthDataHistoricalAccessStartDate(@onNull String packageName)389     public Instant getHealthDataHistoricalAccessStartDate(@NonNull String packageName) {
390         try {
391             long dateMilli =
392                     mService.getHistoricalAccessStartDateInMilliseconds(
393                             packageName, mContext.getUser());
394             if (dateMilli == DEFAULT_LONG) {
395                 return null;
396             } else {
397                 return Instant.ofEpochMilli(dateMilli);
398             }
399         } catch (RemoteException e) {
400             throw e.rethrowFromSystemServer();
401         }
402     }
403 
404     /**
405      * Inserts {@code records} into the HealthConnect database. The records returned in {@link
406      * InsertRecordsResponse} contains the unique IDs of the input records. The values are in same
407      * order as {@code records}. In case of an error or a permission failure the HealthConnect
408      * service, {@link OutcomeReceiver#onError} will be invoked with a {@link
409      * HealthConnectException}.
410      *
411      * @param records list of records to be inserted.
412      * @param executor Executor on which to invoke the callback.
413      * @param callback Callback to receive result of performing this operation.
414      * @throws RuntimeException for internal errors
415      */
insertRecords( @onNull List<Record> records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback)416     public void insertRecords(
417             @NonNull List<Record> records,
418             @NonNull @CallbackExecutor Executor executor,
419             @NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback) {
420         Objects.requireNonNull(records);
421         Objects.requireNonNull(executor);
422         Objects.requireNonNull(callback);
423         try {
424             // Unset any set ids for insert. This is to prevent random string ids from creating
425             // illegal argument exception.
426             records.forEach((record) -> record.getMetadata().setId(""));
427             List<RecordInternal<?>> recordInternals =
428                     records.stream().map(Record::toRecordInternal).collect(Collectors.toList());
429             mService.insertRecords(
430                     mContext.getAttributionSource(),
431                     new RecordsParcel(recordInternals),
432                     new IInsertRecordsResponseCallback.Stub() {
433                         @Override
434                         public void onResult(InsertRecordsResponseParcel parcel) {
435                             Binder.clearCallingIdentity();
436                             executor.execute(
437                                     () ->
438                                             callback.onResult(
439                                                     new InsertRecordsResponse(
440                                                             getRecordsWithUids(
441                                                                     records, parcel.getUids()))));
442                         }
443 
444                         @Override
445                         public void onError(HealthConnectExceptionParcel exception) {
446                             returnError(executor, exception, callback);
447                         }
448                     });
449         } catch (RemoteException e) {
450             throw e.rethrowFromSystemServer();
451         }
452     }
453 
454     /**
455      * Get aggregations corresponding to {@code request}.
456      *
457      * @param <T> Result type of the aggregation.
458      *     <p>Note:
459      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
460      *     typed in nature.
461      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
462      * @param request request for different aggregation.
463      * @param executor Executor on which to invoke the callback.
464      * @param callback Callback to receive result of performing this operation.
465      * @see AggregateRecordsResponse#get
466      */
467     @NonNull
468     @SuppressWarnings("unchecked")
aggregate( @onNull AggregateRecordsRequest<T> request, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<AggregateRecordsResponse<T>, HealthConnectException> callback)469     public <T> void aggregate(
470             @NonNull AggregateRecordsRequest<T> request,
471             @NonNull @CallbackExecutor Executor executor,
472             @NonNull
473                     OutcomeReceiver<AggregateRecordsResponse<T>, HealthConnectException> callback) {
474         Objects.requireNonNull(request);
475         Objects.requireNonNull(executor);
476         Objects.requireNonNull(callback);
477         try {
478             mService.aggregateRecords(
479                     mContext.getAttributionSource(),
480                     new AggregateDataRequestParcel(request),
481                     new IAggregateRecordsResponseCallback.Stub() {
482                         @Override
483                         public void onResult(AggregateDataResponseParcel parcel) {
484                             Binder.clearCallingIdentity();
485                             try {
486                                 executor.execute(
487                                         () ->
488                                                 callback.onResult(
489                                                         (AggregateRecordsResponse<T>)
490                                                                 parcel.getAggregateDataResponse()));
491                             } catch (Exception exception) {
492                                 callback.onError(
493                                         new HealthConnectException(
494                                                 HealthConnectException.ERROR_INTERNAL));
495                             }
496                         }
497 
498                         @Override
499                         public void onError(HealthConnectExceptionParcel exception) {
500                             returnError(executor, exception, callback);
501                         }
502                     });
503         } catch (ClassCastException classCastException) {
504             returnError(
505                     executor,
506                     new HealthConnectExceptionParcel(
507                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
508                     callback);
509         } catch (RemoteException e) {
510             throw e.rethrowFromSystemServer();
511         }
512     }
513 
514     /**
515      * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped
516      * by concrete intervals of time, for example 5 Hrs, 10 Hrs etc.
517      *
518      * @param <T> Result type of the aggregation.
519      *     <p>Note:
520      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
521      *     typed in nature.
522      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
523      * @param request request for different aggregation.
524      * @param duration Duration on which to group by results
525      * @param executor Executor on which to invoke the callback.
526      * @param callback Callback to receive result of performing this operation.
527      * @see HealthConnectManager#aggregateGroupByPeriod
528      */
529     @SuppressWarnings("unchecked")
aggregateGroupByDuration( @onNull AggregateRecordsRequest<T> request, @NonNull Duration duration, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List<AggregateRecordsGroupedByDurationResponse<T>>, HealthConnectException> callback)530     public <T> void aggregateGroupByDuration(
531             @NonNull AggregateRecordsRequest<T> request,
532             @NonNull Duration duration,
533             @NonNull @CallbackExecutor Executor executor,
534             @NonNull
535                     OutcomeReceiver<
536                                     List<AggregateRecordsGroupedByDurationResponse<T>>,
537                                     HealthConnectException>
538                             callback) {
539         Objects.requireNonNull(request);
540         Objects.requireNonNull(duration);
541         Objects.requireNonNull(executor);
542         Objects.requireNonNull(callback);
543         try {
544             mService.aggregateRecords(
545                     mContext.getAttributionSource(),
546                     new AggregateDataRequestParcel(request, duration),
547                     new IAggregateRecordsResponseCallback.Stub() {
548                         @Override
549                         public void onResult(AggregateDataResponseParcel parcel) {
550                             Binder.clearCallingIdentity();
551                             List<AggregateRecordsGroupedByDurationResponse<T>> result =
552                                     new ArrayList<>();
553                             for (AggregateRecordsGroupedByDurationResponse<?>
554                                     aggregateRecordsGroupedByDurationResponse :
555                                             parcel.getAggregateDataResponseGroupedByDuration()) {
556                                 result.add(
557                                         (AggregateRecordsGroupedByDurationResponse<T>)
558                                                 aggregateRecordsGroupedByDurationResponse);
559                             }
560                             executor.execute(() -> callback.onResult(result));
561                         }
562 
563                         @Override
564                         public void onError(HealthConnectExceptionParcel exception) {
565                             returnError(executor, exception, callback);
566                         }
567                     });
568         } catch (ClassCastException classCastException) {
569             returnError(
570                     executor,
571                     new HealthConnectExceptionParcel(
572                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
573                     callback);
574         } catch (RemoteException e) {
575             throw e.rethrowFromSystemServer();
576         }
577     }
578 
579     /**
580      * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped
581      * by number of days. This API handles changes in {@link ZoneOffset} when computing the data on
582      * a per-day basis.
583      *
584      * @param <T> Result type of the aggregation.
585      *     <p>Note:
586      *     <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
587      *     typed in nature.
588      *     <p>Only {@link AggregationType}s that are of same type T can be queried together
589      * @param request Request for different aggregation.
590      * @param period Period on which to group by results
591      * @param executor Executor on which to invoke the callback.
592      * @param callback Callback to receive result of performing this operation.
593      * @see AggregateRecordsGroupedByPeriodResponse#get
594      * @see HealthConnectManager#aggregateGroupByDuration
595      */
596     @SuppressWarnings("unchecked")
aggregateGroupByPeriod( @onNull AggregateRecordsRequest<T> request, @NonNull Period period, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List<AggregateRecordsGroupedByPeriodResponse<T>>, HealthConnectException> callback)597     public <T> void aggregateGroupByPeriod(
598             @NonNull AggregateRecordsRequest<T> request,
599             @NonNull Period period,
600             @NonNull @CallbackExecutor Executor executor,
601             @NonNull
602                     OutcomeReceiver<
603                                     List<AggregateRecordsGroupedByPeriodResponse<T>>,
604                                     HealthConnectException>
605                             callback) {
606         Objects.requireNonNull(request);
607         Objects.requireNonNull(period);
608         Objects.requireNonNull(executor);
609         Objects.requireNonNull(callback);
610         try {
611             mService.aggregateRecords(
612                     mContext.getAttributionSource(),
613                     new AggregateDataRequestParcel(request, period),
614                     new IAggregateRecordsResponseCallback.Stub() {
615                         @Override
616                         public void onResult(AggregateDataResponseParcel parcel) {
617                             Binder.clearCallingIdentity();
618                             List<AggregateRecordsGroupedByPeriodResponse<T>> result =
619                                     new ArrayList<>();
620                             for (AggregateRecordsGroupedByPeriodResponse<?>
621                                     aggregateRecordsGroupedByPeriodResponse :
622                                             parcel.getAggregateDataResponseGroupedByPeriod()) {
623                                 result.add(
624                                         (AggregateRecordsGroupedByPeriodResponse<T>)
625                                                 aggregateRecordsGroupedByPeriodResponse);
626                             }
627 
628                             executor.execute(() -> callback.onResult(result));
629                         }
630 
631                         @Override
632                         public void onError(HealthConnectExceptionParcel exception) {
633                             returnError(executor, exception, callback);
634                         }
635                     });
636         } catch (ClassCastException classCastException) {
637             returnError(
638                     executor,
639                     new HealthConnectExceptionParcel(
640                             new HealthConnectException(HealthConnectException.ERROR_INTERNAL)),
641                     callback);
642         } catch (RemoteException e) {
643             throw e.rethrowFromSystemServer();
644         }
645     }
646 
647     /**
648      * Deletes records based on the {@link DeleteUsingFiltersRequest}. This is only to be used by
649      * health connect controller APK(s). Ids that don't exist will be ignored.
650      *
651      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
652      *
653      * @param request Request based on which to perform delete operation
654      * @param executor Executor on which to invoke the callback.
655      * @param callback Callback to receive result of performing this operation.
656      * @hide
657      */
658     @SystemApi
659     @RequiresPermission(MANAGE_HEALTH_PERMISSIONS)
deleteRecords( @onNull DeleteUsingFiltersRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)660     public void deleteRecords(
661             @NonNull DeleteUsingFiltersRequest request,
662             @NonNull Executor executor,
663             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
664         Objects.requireNonNull(request);
665         Objects.requireNonNull(executor);
666         Objects.requireNonNull(callback);
667 
668         try {
669             mService.deleteUsingFilters(
670                     mContext.getAttributionSource(),
671                     new DeleteUsingFiltersRequestParcel(request),
672                     new IEmptyResponseCallback.Stub() {
673                         @Override
674                         public void onResult() {
675                             executor.execute(() -> callback.onResult(null));
676                         }
677 
678                         @Override
679                         public void onError(HealthConnectExceptionParcel exception) {
680                             returnError(executor, exception, callback);
681                         }
682                     });
683         } catch (RemoteException remoteException) {
684             remoteException.rethrowFromSystemServer();
685         }
686     }
687 
688     /**
689      * Deletes records based on {@link RecordIdFilter}.
690      *
691      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
692      *
693      * @param recordIds recordIds on which to perform delete operation.
694      * @param executor Executor on which to invoke the callback.
695      * @param callback Callback to receive result of performing this operation.
696      * @throws IllegalArgumentException if {@code recordIds is empty}
697      */
deleteRecords( @onNull List<RecordIdFilter> recordIds, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)698     public void deleteRecords(
699             @NonNull List<RecordIdFilter> recordIds,
700             @NonNull Executor executor,
701             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
702         Objects.requireNonNull(recordIds);
703         Objects.requireNonNull(executor);
704         Objects.requireNonNull(callback);
705 
706         if (recordIds.isEmpty()) {
707             throw new IllegalArgumentException("record ids can't be empty");
708         }
709 
710         try {
711             mService.deleteUsingFiltersForSelf(
712                     mContext.getAttributionSource(),
713                     new DeleteUsingFiltersRequestParcel(
714                             new RecordIdFiltersParcel(recordIds), mContext.getPackageName()),
715                     new IEmptyResponseCallback.Stub() {
716                         @Override
717                         public void onResult() {
718                             executor.execute(() -> callback.onResult(null));
719                         }
720 
721                         @Override
722                         public void onError(HealthConnectExceptionParcel exception) {
723                             returnError(executor, exception, callback);
724                         }
725                     });
726         } catch (RemoteException remoteException) {
727             remoteException.rethrowFromSystemServer();
728         }
729     }
730 
731     /**
732      * Deletes records based on the {@link TimeRangeFilter}.
733      *
734      * <p>Deletions are performed in a transaction i.e. either all will be deleted or none
735      *
736      * @param recordType recordType to perform delete operation on.
737      * @param timeRangeFilter time filter based on which to delete the records.
738      * @param executor Executor on which to invoke the callback.
739      * @param callback Callback to receive result of performing this operation.
740      */
deleteRecords( @onNull Class<? extends Record> recordType, @NonNull TimeRangeFilter timeRangeFilter, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)741     public void deleteRecords(
742             @NonNull Class<? extends Record> recordType,
743             @NonNull TimeRangeFilter timeRangeFilter,
744             @NonNull Executor executor,
745             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
746         Objects.requireNonNull(recordType);
747         Objects.requireNonNull(timeRangeFilter);
748         Objects.requireNonNull(executor);
749         Objects.requireNonNull(callback);
750 
751         try {
752             mService.deleteUsingFiltersForSelf(
753                     mContext.getAttributionSource(),
754                     new DeleteUsingFiltersRequestParcel(
755                             new DeleteUsingFiltersRequest.Builder()
756                                     .addDataOrigin(
757                                             new DataOrigin.Builder()
758                                                     .setPackageName(mContext.getPackageName())
759                                                     .build())
760                                     .addRecordType(recordType)
761                                     .setTimeRangeFilter(timeRangeFilter)
762                                     .build()),
763                     new IEmptyResponseCallback.Stub() {
764                         @Override
765                         public void onResult() {
766                             executor.execute(() -> callback.onResult(null));
767                         }
768 
769                         @Override
770                         public void onError(HealthConnectExceptionParcel exception) {
771                             returnError(executor, exception, callback);
772                         }
773                     });
774         } catch (RemoteException remoteException) {
775             remoteException.rethrowFromSystemServer();
776         }
777     }
778 
779     /**
780      * Get change logs post the time when {@code token} was generated.
781      *
782      * @param changeLogsRequest The token from {@link HealthConnectManager#getChangeLogToken}.
783      * @param executor Executor on which to invoke the callback.
784      * @param callback Callback to receive result of performing this operation.
785      * @see HealthConnectManager#getChangeLogToken
786      */
getChangeLogs( @onNull ChangeLogsRequest changeLogsRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback)787     public void getChangeLogs(
788             @NonNull ChangeLogsRequest changeLogsRequest,
789             @NonNull @CallbackExecutor Executor executor,
790             @NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback) {
791         Objects.requireNonNull(changeLogsRequest);
792         Objects.requireNonNull(executor);
793         Objects.requireNonNull(callback);
794 
795         try {
796             mService.getChangeLogs(
797                     mContext.getAttributionSource(),
798                     changeLogsRequest,
799                     new IChangeLogsResponseCallback.Stub() {
800                         @Override
801                         public void onResult(ChangeLogsResponse parcel) {
802                             Binder.clearCallingIdentity();
803                             executor.execute(() -> callback.onResult(parcel));
804                         }
805 
806                         @Override
807                         public void onError(HealthConnectExceptionParcel exception) {
808                             returnError(executor, exception, callback);
809                         }
810                     });
811         } catch (ClassCastException invalidArgumentException) {
812             callback.onError(
813                     new HealthConnectException(
814                             HealthConnectException.ERROR_INVALID_ARGUMENT,
815                             invalidArgumentException.getMessage()));
816         } catch (RemoteException e) {
817             throw e.rethrowFromSystemServer();
818         }
819     }
820 
821     /**
822      * Get token for {HealthConnectManager#getChangeLogs}. Changelogs requested corresponding to
823      * this token will be post the time this token was generated by the system all items that match
824      * the given filters.
825      *
826      * <p>Tokens from this request are to be passed to {HealthConnectManager#getChangeLogs}
827      *
828      * @param request A request to get changelog token
829      * @param executor Executor on which to invoke the callback.
830      * @param callback Callback to receive result of performing this operation.
831      */
getChangeLogToken( @onNull ChangeLogTokenRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<ChangeLogTokenResponse, HealthConnectException> callback)832     public void getChangeLogToken(
833             @NonNull ChangeLogTokenRequest request,
834             @NonNull Executor executor,
835             @NonNull OutcomeReceiver<ChangeLogTokenResponse, HealthConnectException> callback) {
836         try {
837             mService.getChangeLogToken(
838                     mContext.getAttributionSource(),
839                     request,
840                     new IGetChangeLogTokenCallback.Stub() {
841                         @Override
842                         public void onResult(ChangeLogTokenResponse parcel) {
843                             Binder.clearCallingIdentity();
844                             executor.execute(() -> callback.onResult(parcel));
845                         }
846 
847                         @Override
848                         public void onError(HealthConnectExceptionParcel exception) {
849                             returnError(executor, exception, callback);
850                         }
851                     });
852         } catch (RemoteException e) {
853             throw e.rethrowFromSystemServer();
854         }
855     }
856 
857     /**
858      * Fetch the data priority order of the contributing {@link DataOrigin} for {@code
859      * dataCategory}.
860      *
861      * @param dataCategory {@link HealthDataCategory} for which to get the priority order
862      * @param executor Executor on which to invoke the callback.
863      * @param callback Callback to receive result of performing this operation.
864      * @hide
865      */
866     @SystemApi
867     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
fetchDataOriginsPriorityOrder( @ealthDataCategory.Type int dataCategory, @NonNull Executor executor, @NonNull OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, HealthConnectException> callback)868     public void fetchDataOriginsPriorityOrder(
869             @HealthDataCategory.Type int dataCategory,
870             @NonNull Executor executor,
871             @NonNull
872                     OutcomeReceiver<FetchDataOriginsPriorityOrderResponse, HealthConnectException>
873                             callback) {
874         try {
875             mService.getCurrentPriority(
876                     mContext.getPackageName(),
877                     dataCategory,
878                     new IGetPriorityResponseCallback.Stub() {
879                         @Override
880                         public void onResult(GetPriorityResponseParcel response) {
881                             Binder.clearCallingIdentity();
882                             executor.execute(
883                                     () -> callback.onResult(response.getPriorityResponse()));
884                         }
885 
886                         @Override
887                         public void onError(HealthConnectExceptionParcel exception) {
888                             returnError(executor, exception, callback);
889                         }
890                     });
891         } catch (RemoteException e) {
892             throw e.rethrowFromSystemServer();
893         }
894     }
895 
896     /**
897      * Updates the priority order of the apps as per {@code request}
898      *
899      * @param request new priority order update request
900      * @param executor Executor on which to invoke the callback.
901      * @param callback Callback to receive result of performing this operation.
902      * @hide
903      */
904     @SystemApi
905     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
updateDataOriginPriorityOrder( @onNull UpdateDataOriginPriorityOrderRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)906     public void updateDataOriginPriorityOrder(
907             @NonNull UpdateDataOriginPriorityOrderRequest request,
908             @NonNull Executor executor,
909             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
910         try {
911             mService.updatePriority(
912                     mContext.getPackageName(),
913                     new UpdatePriorityRequestParcel(request),
914                     new IEmptyResponseCallback.Stub() {
915                         @Override
916                         public void onResult() {
917                             Binder.clearCallingIdentity();
918                             executor.execute(() -> callback.onResult(null));
919                         }
920 
921                         @Override
922                         public void onError(HealthConnectExceptionParcel exception) {
923                             returnError(executor, exception, callback);
924                         }
925                     });
926         } catch (RemoteException e) {
927             throw e.rethrowFromSystemServer();
928         }
929     }
930 
931     /**
932      * Retrieves {@link RecordTypeInfoResponse} for each RecordType.
933      *
934      * @param executor Executor on which to invoke the callback.
935      * @param callback Callback to receive result of performing this operation.
936      * @hide
937      */
938     @SystemApi
939     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryAllRecordTypesInfo( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver< Map<Class<? extends Record>, RecordTypeInfoResponse>, HealthConnectException> callback)940     public void queryAllRecordTypesInfo(
941             @NonNull @CallbackExecutor Executor executor,
942             @NonNull
943                     OutcomeReceiver<
944                                     Map<Class<? extends Record>, RecordTypeInfoResponse>,
945                                     HealthConnectException>
946                             callback) {
947         Objects.requireNonNull(executor);
948         Objects.requireNonNull(callback);
949         try {
950             mService.queryAllRecordTypesInfo(
951                     new IRecordTypeInfoResponseCallback.Stub() {
952                         @Override
953                         public void onResult(RecordTypeInfoResponseParcel parcel) {
954                             Binder.clearCallingIdentity();
955                             executor.execute(
956                                     () -> callback.onResult(parcel.getRecordTypeInfoResponses()));
957                         }
958 
959                         @Override
960                         public void onError(HealthConnectExceptionParcel exception) {
961                             returnError(executor, exception, callback);
962                         }
963                     });
964         } catch (RemoteException e) {
965             throw e.rethrowFromSystemServer();
966         }
967     }
968 
969     /**
970      * Returns currently set auto delete period for this user.
971      *
972      * <p>If you are calling this function for the first time after a user unlock, this might take
973      * some time so consider calling this on a thread.
974      *
975      * @return Auto delete period in days, 0 is returned if auto delete period is not set.
976      * @throws RuntimeException for internal errors
977      * @hide
978      */
979     @SystemApi
980     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
981     @IntRange(from = 0, to = 7300)
getRecordRetentionPeriodInDays()982     public int getRecordRetentionPeriodInDays() {
983         try {
984             return mService.getRecordRetentionPeriodInDays(mContext.getUser());
985         } catch (RemoteException e) {
986             throw e.rethrowFromSystemServer();
987         }
988     }
989 
990     /**
991      * Sets auto delete period (for all the records to be automatically deleted) for this user.
992      *
993      * <p>Note: The max value of auto delete period can be 7300 i.e. ~20 years
994      *
995      * @param days Auto period to be set in days. Use 0 to unset this value.
996      * @param executor Executor on which to invoke the callback.
997      * @param callback Callback to receive result of performing this operation.
998      * @throws RuntimeException for internal errors
999      * @throws IllegalArgumentException if {@code days} is not between 0 and 7300
1000      * @hide
1001      */
1002     @SystemApi
1003     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
setRecordRetentionPeriodInDays( @ntRangefrom = 0, to = 7300) int days, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1004     public void setRecordRetentionPeriodInDays(
1005             @IntRange(from = 0, to = 7300) int days,
1006             @NonNull Executor executor,
1007             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1008         Objects.requireNonNull(executor);
1009         Objects.requireNonNull(callback);
1010 
1011         if (days < 0 || days > 7300) {
1012             throw new IllegalArgumentException("days should be between " + 0 + " and " + 7300);
1013         }
1014 
1015         try {
1016             mService.setRecordRetentionPeriodInDays(
1017                     days,
1018                     mContext.getUser(),
1019                     new IEmptyResponseCallback.Stub() {
1020                         @Override
1021                         public void onResult() {
1022                             Binder.clearCallingIdentity();
1023                             executor.execute(() -> callback.onResult(null));
1024                         }
1025 
1026                         @Override
1027                         public void onError(HealthConnectExceptionParcel exception) {
1028                             returnError(executor, exception, callback);
1029                         }
1030                     });
1031         } catch (RemoteException e) {
1032             e.rethrowFromSystemServer();
1033         }
1034     }
1035 
1036     /**
1037      * Returns a list of access logs with package name and its access time for each record type.
1038      *
1039      * @param executor Executor on which to invoke the callback.
1040      * @param callback Callback to receive result of performing this operation.
1041      * @hide
1042      */
1043     @SystemApi
1044     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryAccessLogs( @onNull Executor executor, @NonNull OutcomeReceiver<List<AccessLog>, HealthConnectException> callback)1045     public void queryAccessLogs(
1046             @NonNull Executor executor,
1047             @NonNull OutcomeReceiver<List<AccessLog>, HealthConnectException> callback) {
1048         Objects.requireNonNull(executor);
1049         Objects.requireNonNull(callback);
1050         try {
1051             mService.queryAccessLogs(
1052                     mContext.getPackageName(),
1053                     new IAccessLogsResponseCallback.Stub() {
1054                         @Override
1055                         public void onResult(AccessLogsResponseParcel parcel) {
1056                             Binder.clearCallingIdentity();
1057                             executor.execute(() -> callback.onResult(parcel.getAccessLogs()));
1058                         }
1059 
1060                         @Override
1061                         public void onError(HealthConnectExceptionParcel exception) {
1062                             returnError(executor, exception, callback);
1063                         }
1064                     });
1065         } catch (RemoteException e) {
1066             throw e.rethrowFromSystemServer();
1067         }
1068     }
1069 
1070     /**
1071      * API to read records based on {@link ReadRecordsRequestUsingFilters} or {@link
1072      * ReadRecordsRequestUsingIds}
1073      *
1074      * <p>Number of records returned by this API will depend based on below factors:
1075      *
1076      * <p>When an app with read permission allowed calls the API from background then it will be
1077      * able to read only its own inserted records and will not get records inserted by other apps.
1078      * This may be less than the total records present for the record type.
1079      *
1080      * <p>When an app with read permission allowed calls the API from foreground then it will be
1081      * able to read all records for the record type.
1082      *
1083      * <p>App with only write permission but no read permission allowed will be able to read only
1084      * its own inserted records both when in foreground or background.
1085      *
1086      * <p>An app without both read and write permissions will not be able to read any record and the
1087      * API will throw Security Exception.
1088      *
1089      * @param request Read request based on {@link ReadRecordsRequestUsingFilters} or {@link
1090      *     ReadRecordsRequestUsingIds}
1091      * @param executor Executor on which to invoke the callback.
1092      * @param callback Callback to receive result of performing this operation.
1093      * @throws IllegalArgumentException if request page size set is more than 5000 in {@link
1094      *     ReadRecordsRequestUsingFilters}
1095      * @throws SecurityException if app without read or write permission tries to read.
1096      */
readRecords( @onNull ReadRecordsRequest<T> request, @NonNull Executor executor, @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback)1097     public <T extends Record> void readRecords(
1098             @NonNull ReadRecordsRequest<T> request,
1099             @NonNull Executor executor,
1100             @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
1101         Objects.requireNonNull(request);
1102         Objects.requireNonNull(executor);
1103         Objects.requireNonNull(callback);
1104         try {
1105             mService.readRecords(
1106                     mContext.getAttributionSource(),
1107                     request.toReadRecordsRequestParcel(),
1108                     getReadCallback(executor, callback));
1109         } catch (RemoteException remoteException) {
1110             remoteException.rethrowFromSystemServer();
1111         }
1112     }
1113 
1114     /**
1115      * Updates {@code records} into the HealthConnect database. In case of an error or a permission
1116      * failure the HealthConnect service, {@link OutcomeReceiver#onError} will be invoked with a
1117      * {@link HealthConnectException}.
1118      *
1119      * <p>In case the input record to be updated does not exist in the database or the caller is not
1120      * the owner of the record then {@link HealthConnectException#ERROR_INVALID_ARGUMENT} will be
1121      * thrown.
1122      *
1123      * @param records list of records to be updated.
1124      * @param executor Executor on which to invoke the callback.
1125      * @param callback Callback to receive result of performing this operation.
1126      * @throws IllegalArgumentException if at least one of the records is missing both
1127      *     ClientRecordID and UUID.
1128      */
updateRecords( @onNull List<Record> records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, HealthConnectException> callback)1129     public void updateRecords(
1130             @NonNull List<Record> records,
1131             @NonNull @CallbackExecutor Executor executor,
1132             @NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
1133         Objects.requireNonNull(records);
1134         Objects.requireNonNull(executor);
1135         Objects.requireNonNull(callback);
1136         try {
1137             List<RecordInternal<?>> recordInternals =
1138                     records.stream().map(Record::toRecordInternal).collect(Collectors.toList());
1139             // Verify if the input record has clientRecordId or UUID.
1140             for (RecordInternal<?> recordInternal : recordInternals) {
1141                 if ((recordInternal.getClientRecordId() == null
1142                                 || recordInternal.getClientRecordId().isEmpty())
1143                         && recordInternal.getUuid() == null) {
1144                     throw new IllegalArgumentException(
1145                             "At least one of the records is missing both ClientRecordID"
1146                                     + " and UUID. RecordType of the input: "
1147                                     + recordInternal.getRecordType());
1148                 }
1149             }
1150 
1151             mService.updateRecords(
1152                     mContext.getAttributionSource(),
1153                     new RecordsParcel(recordInternals),
1154                     new IEmptyResponseCallback.Stub() {
1155                         @Override
1156                         public void onResult() {
1157                             Binder.clearCallingIdentity();
1158                             executor.execute(() -> callback.onResult(null));
1159                         }
1160 
1161                         @Override
1162                         public void onError(HealthConnectExceptionParcel exception) {
1163                             Binder.clearCallingIdentity();
1164                             callback.onError(exception.getHealthConnectException());
1165                         }
1166                     });
1167         } catch (ArithmeticException
1168                 | ClassCastException
1169                 | IllegalArgumentException invalidArgumentException) {
1170             throw new IllegalArgumentException(invalidArgumentException);
1171         } catch (RemoteException e) {
1172             throw e.rethrowFromSystemServer();
1173         }
1174     }
1175 
1176     /**
1177      * Returns information, represented by {@code ApplicationInfoResponse}, for all the packages
1178      * that have contributed to the health connect DB. If the application is does not have
1179      * permissions to query other packages, a {@link java.lang.SecurityException} is thrown.
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     @NonNull
1186     @SystemApi
1187     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
getContributorApplicationsInfo( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback)1188     public void getContributorApplicationsInfo(
1189             @NonNull @CallbackExecutor Executor executor,
1190             @NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback) {
1191         Objects.requireNonNull(executor);
1192         Objects.requireNonNull(callback);
1193 
1194         try {
1195             mService.getContributorApplicationsInfo(
1196                     new IApplicationInfoResponseCallback.Stub() {
1197                         @Override
1198                         public void onResult(ApplicationInfoResponseParcel parcel) {
1199                             Binder.clearCallingIdentity();
1200                             executor.execute(
1201                                     () ->
1202                                             callback.onResult(
1203                                                     new ApplicationInfoResponse(
1204                                                             parcel.getAppInfoList())));
1205                         }
1206 
1207                         @Override
1208                         public void onError(HealthConnectExceptionParcel exception) {
1209                             returnError(executor, exception, callback);
1210                         }
1211                     });
1212 
1213         } catch (RemoteException e) {
1214             throw e.rethrowFromSystemServer();
1215         }
1216     }
1217 
1218     /**
1219      * Stages all HealthConnect remote data and returns any errors in a callback. Errors encountered
1220      * for all the files are shared in the provided callback. Any authorization / permissions
1221      * related error is reported to the callback with an empty file name.
1222      *
1223      * <p>The staged data will later be restored (integrated) into the existing Health Connect data.
1224      * Any existing data will not be affected by the staged data.
1225      *
1226      * <p>The file names passed should be the same as the ones on the original device that were
1227      * backed up or are being transferred directly.
1228      *
1229      * <p>If a file already exists in the staged data then it will be replaced. However, note that
1230      * staging data is a one time process. And if the staged data has already been processed then
1231      * any attempt to stage data again will be silently ignored.
1232      *
1233      * <p>The caller is responsible for closing the original file descriptors. The file descriptors
1234      * are duplicated and the originals may be closed by the application at any time after this API
1235      * returns.
1236      *
1237      * <p>The caller should update the data download states using {@link #updateDataDownloadState}
1238      * before calling this API.
1239      *
1240      * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s.
1241      * @param executor The {@link Executor} on which to invoke the callback.
1242      * @param callback The callback which will receive the outcome of this call.
1243      * @hide
1244      */
1245     @SystemApi
1246     @UserHandleAware
1247     @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA)
stageAllHealthConnectRemoteData( @onNull Map<String, ParcelFileDescriptor> pfdsByFileName, @NonNull Executor executor, @NonNull OutcomeReceiver<Void, StageRemoteDataException> callback)1248     public void stageAllHealthConnectRemoteData(
1249             @NonNull Map<String, ParcelFileDescriptor> pfdsByFileName,
1250             @NonNull Executor executor,
1251             @NonNull OutcomeReceiver<Void, StageRemoteDataException> callback)
1252             throws NullPointerException {
1253         Objects.requireNonNull(pfdsByFileName);
1254         Objects.requireNonNull(executor);
1255         Objects.requireNonNull(callback);
1256 
1257         try {
1258             mService.stageAllHealthConnectRemoteData(
1259                     new StageRemoteDataRequest(pfdsByFileName),
1260                     mContext.getUser(),
1261                     new IDataStagingFinishedCallback.Stub() {
1262                         @Override
1263                         public void onResult() {
1264                             Binder.clearCallingIdentity();
1265                             executor.execute(() -> callback.onResult(null));
1266                         }
1267 
1268                         @Override
1269                         public void onError(StageRemoteDataException stageRemoteDataException) {
1270                             Binder.clearCallingIdentity();
1271                             executor.execute(() -> callback.onError(stageRemoteDataException));
1272                         }
1273                     });
1274         } catch (RemoteException e) {
1275             throw e.rethrowFromSystemServer();
1276         }
1277     }
1278 
1279     /**
1280      * Copies all HealthConnect backup data in the passed FDs.
1281      *
1282      * <p>The shared data must later be sent for Backup to cloud or another device.
1283      *
1284      * <p>We are responsible for closing the original file descriptors. The caller must not close
1285      * the FD before that.
1286      *
1287      * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s.
1288      * @hide
1289      */
getAllDataForBackup(@onNull Map<String, ParcelFileDescriptor> pfdsByFileName)1290     public void getAllDataForBackup(@NonNull Map<String, ParcelFileDescriptor> pfdsByFileName) {
1291         Objects.requireNonNull(pfdsByFileName);
1292 
1293         try {
1294             mService.getAllDataForBackup(
1295                     new StageRemoteDataRequest(pfdsByFileName), mContext.getUser());
1296         } catch (RemoteException e) {
1297             throw e.rethrowFromSystemServer();
1298         }
1299     }
1300 
1301     /**
1302      * Returns the names of all HealthConnect Backup files
1303      *
1304      * @hide
1305      */
getAllBackupFileNames(boolean forDeviceToDevice)1306     public Set<String> getAllBackupFileNames(boolean forDeviceToDevice) {
1307         try {
1308             return mService.getAllBackupFileNames(forDeviceToDevice).getFileNames();
1309         } catch (RemoteException e) {
1310             throw e.rethrowFromSystemServer();
1311         }
1312     }
1313 
1314     /**
1315      * Deletes all previously staged HealthConnect data from the disk. For testing purposes only.
1316      *
1317      * <p>This deletes only the staged data leaving any other Health Connect data untouched.
1318      *
1319      * @hide
1320      */
1321     @TestApi
1322     @UserHandleAware
deleteAllStagedRemoteData()1323     public void deleteAllStagedRemoteData() throws NullPointerException {
1324         try {
1325             mService.deleteAllStagedRemoteData(mContext.getUser());
1326         } catch (RemoteException e) {
1327             throw e.rethrowFromSystemServer();
1328         }
1329     }
1330 
1331     /**
1332      * Updates the download state of the Health Connect data.
1333      *
1334      * <p>The data should've been downloaded and the corresponding download states updated before
1335      * the app calls {@link #stageAllHealthConnectRemoteData}. Once {@link
1336      * #stageAllHealthConnectRemoteData} has been called the downloaded state becomes {@link
1337      * #DATA_DOWNLOAD_COMPLETE} and future attempts to update the download state are ignored.
1338      *
1339      * <p>The only valid order of state transition are:
1340      *
1341      * <ul>
1342      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_COMPLETE}
1343      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
1344      *       #DATA_DOWNLOAD_COMPLETE}
1345      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_FAILED}
1346      *   <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
1347      *       #DATA_DOWNLOAD_FAILED}
1348      * </ul>
1349      *
1350      * <p>Note that it's okay if some states are missing in of the sequences above but the order has
1351      * to be one of the above.
1352      *
1353      * <p>Only one app will have the permission to call this API so it is assured that no one else
1354      * will be able to update this state.
1355      *
1356      * @param downloadState The download state which needs to be purely from {@link
1357      *     DataDownloadState}
1358      * @hide
1359      */
1360     @SystemApi
1361     @UserHandleAware
1362     @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA)
updateDataDownloadState(@ataDownloadState int downloadState)1363     public void updateDataDownloadState(@DataDownloadState int downloadState) {
1364         try {
1365             mService.updateDataDownloadState(downloadState);
1366         } catch (RemoteException e) {
1367             throw e.rethrowFromSystemServer();
1368         }
1369     }
1370 
1371     /**
1372      * Asynchronously returns the current UI state of Health Connect as it goes through the
1373      * Data-Migration process. In case there was an error reading the data on the disk the error
1374      * will be returned in the callback.
1375      *
1376      * <p>See also {@link HealthConnectMigrationUiState} object describing the HealthConnect UI
1377      * state.
1378      *
1379      * @param executor The {@link Executor} on which to invoke the callback.
1380      * @param callback The callback which will receive the current {@link
1381      *     HealthConnectMigrationUiState} or the {@link HealthConnectException}.
1382      * @hide
1383      */
1384     @UserHandleAware
1385     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
1386     @NonNull
getHealthConnectMigrationUiState( @onNull Executor executor, @NonNull OutcomeReceiver<HealthConnectMigrationUiState, HealthConnectException> callback)1387     public void getHealthConnectMigrationUiState(
1388             @NonNull Executor executor,
1389             @NonNull
1390                     OutcomeReceiver<HealthConnectMigrationUiState, HealthConnectException>
1391                             callback) {
1392         Objects.requireNonNull(executor);
1393         Objects.requireNonNull(callback);
1394 
1395         try {
1396             mService.getHealthConnectMigrationUiState(
1397                     new IGetHealthConnectMigrationUiStateCallback.Stub() {
1398                         @Override
1399                         public void onResult(HealthConnectMigrationUiState migrationUiState) {
1400                             Binder.clearCallingIdentity();
1401                             executor.execute(() -> callback.onResult(migrationUiState));
1402                         }
1403 
1404                         @Override
1405                         public void onError(HealthConnectExceptionParcel exception) {
1406                             Binder.clearCallingIdentity();
1407                             executor.execute(
1408                                     () -> callback.onError(exception.getHealthConnectException()));
1409                         }
1410                     });
1411         } catch (RemoteException e) {
1412             throw e.rethrowFromSystemServer();
1413         }
1414     }
1415 
1416     /**
1417      * Asynchronously returns the current state of the Health Connect data as it goes through the
1418      * Data-Restore and/or the Data-Migration process. In case there was an error reading the data
1419      * on the disk the error will be returned in the callback.
1420      *
1421      * <p>See also {@link HealthConnectDataState} object describing the HealthConnect state.
1422      *
1423      * @param executor The {@link Executor} on which to invoke the callback.
1424      * @param callback The callback which will receive the current {@link HealthConnectDataState} or
1425      *     the {@link HealthConnectException}.
1426      * @hide
1427      */
1428     @SystemApi
1429     @UserHandleAware
1430     @RequiresPermission(
1431             anyOf = {
1432                 MANAGE_HEALTH_DATA_PERMISSION,
1433                 Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA
1434             })
1435     @NonNull
getHealthConnectDataState( @onNull Executor executor, @NonNull OutcomeReceiver<HealthConnectDataState, HealthConnectException> callback)1436     public void getHealthConnectDataState(
1437             @NonNull Executor executor,
1438             @NonNull OutcomeReceiver<HealthConnectDataState, HealthConnectException> callback) {
1439         Objects.requireNonNull(executor);
1440         Objects.requireNonNull(callback);
1441         try {
1442             mService.getHealthConnectDataState(
1443                     new IGetHealthConnectDataStateCallback.Stub() {
1444                         @Override
1445                         public void onResult(HealthConnectDataState healthConnectDataState) {
1446                             Binder.clearCallingIdentity();
1447                             executor.execute(() -> callback.onResult(healthConnectDataState));
1448                         }
1449 
1450                         @Override
1451                         public void onError(HealthConnectExceptionParcel exception) {
1452                             Binder.clearCallingIdentity();
1453                             executor.execute(
1454                                     () -> callback.onError(exception.getHealthConnectException()));
1455                         }
1456                     });
1457         } catch (RemoteException e) {
1458             throw e.rethrowFromSystemServer();
1459         }
1460     }
1461 
1462     /**
1463      * Returns a list of unique dates for which the DB has at least one entry.
1464      *
1465      * @param recordTypes List of record types classes for which to get the activity dates.
1466      * @param executor Executor on which to invoke the callback.
1467      * @param callback Callback to receive result of performing this operation.
1468      * @throws java.lang.IllegalArgumentException If the record types list is empty.
1469      * @hide
1470      */
1471     @NonNull
1472     @SystemApi
1473     @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION)
queryActivityDates( @onNull List<Class<? extends Record>> recordTypes, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<List<LocalDate>, HealthConnectException> callback)1474     public void queryActivityDates(
1475             @NonNull List<Class<? extends Record>> recordTypes,
1476             @NonNull @CallbackExecutor Executor executor,
1477             @NonNull OutcomeReceiver<List<LocalDate>, HealthConnectException> callback) {
1478         Objects.requireNonNull(executor);
1479         Objects.requireNonNull(callback);
1480         Objects.requireNonNull(recordTypes);
1481 
1482         if (recordTypes.isEmpty()) {
1483             throw new IllegalArgumentException("Record types list can not be empty");
1484         }
1485 
1486         try {
1487             mService.getActivityDates(
1488                     new ActivityDatesRequestParcel(recordTypes),
1489                     new IActivityDatesResponseCallback.Stub() {
1490                         @Override
1491                         public void onResult(ActivityDatesResponseParcel parcel) {
1492                             Binder.clearCallingIdentity();
1493                             executor.execute(() -> callback.onResult(parcel.getDates()));
1494                         }
1495 
1496                         @Override
1497                         public void onError(HealthConnectExceptionParcel exception) {
1498                             returnError(executor, exception, callback);
1499                         }
1500                     });
1501 
1502         } catch (RemoteException exception) {
1503             exception.rethrowFromSystemServer();
1504         }
1505     }
1506 
1507     /**
1508      * Marks the start of the migration and block API calls.
1509      *
1510      * @param executor Executor on which to invoke the callback.
1511      * @param callback Callback to receive result of performing this operation.
1512      * @hide
1513      */
1514     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1515     @SystemApi
startMigration( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1516     public void startMigration(
1517             @NonNull @CallbackExecutor Executor executor,
1518             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1519         Objects.requireNonNull(executor);
1520         Objects.requireNonNull(callback);
1521         try {
1522             mService.startMigration(
1523                     mContext.getPackageName(), wrapMigrationCallback(executor, callback));
1524         } catch (RemoteException e) {
1525             throw e.rethrowFromSystemServer();
1526         }
1527     }
1528 
1529     /**
1530      * Marks the end of the migration.
1531      *
1532      * @param executor Executor on which to invoke the callback.
1533      * @param callback Callback to receive result of performing this operation.
1534      * @hide
1535      */
1536     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1537     @SystemApi
finishMigration( @onNull @allbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1538     public void finishMigration(
1539             @NonNull @CallbackExecutor Executor executor,
1540             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1541         Objects.requireNonNull(executor);
1542         Objects.requireNonNull(callback);
1543         try {
1544             mService.finishMigration(
1545                     mContext.getPackageName(), wrapMigrationCallback(executor, callback));
1546         } catch (RemoteException e) {
1547             throw e.rethrowFromSystemServer();
1548         }
1549     }
1550 
1551     /**
1552      * Writes data to the module database.
1553      *
1554      * @param entities List of {@link MigrationEntity} to migrate.
1555      * @param executor Executor on which to invoke the callback.
1556      * @param callback Callback to receive result of performing this operation.
1557      * @hide
1558      */
1559     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
1560     @SystemApi
writeMigrationData( @onNull List<MigrationEntity> entities, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1561     public void writeMigrationData(
1562             @NonNull List<MigrationEntity> entities,
1563             @NonNull @CallbackExecutor Executor executor,
1564             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1565 
1566         Objects.requireNonNull(entities);
1567         Objects.requireNonNull(executor);
1568         Objects.requireNonNull(callback);
1569 
1570         try {
1571             mService.writeMigrationData(
1572                     mContext.getPackageName(),
1573                     new MigrationEntityParcel(entities),
1574                     wrapMigrationCallback(executor, callback));
1575         } catch (RemoteException e) {
1576             throw e.rethrowFromSystemServer();
1577         }
1578     }
1579 
1580     /**
1581      * Sets the minimum version on which the module will inform the migrator package of its
1582      * migration readiness.
1583      *
1584      * @param executor Executor on which to invoke the callback.
1585      * @param callback Callback to receive result of performing this operation.
1586      * @hide
1587      */
1588     @SystemApi
1589     @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA)
insertMinDataMigrationSdkExtensionVersion( int requiredSdkExtension, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver<Void, MigrationException> callback)1590     public void insertMinDataMigrationSdkExtensionVersion(
1591             int requiredSdkExtension,
1592             @NonNull @CallbackExecutor Executor executor,
1593             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1594         Objects.requireNonNull(executor);
1595         Objects.requireNonNull(callback);
1596         try {
1597             mService.insertMinDataMigrationSdkExtensionVersion(
1598                     mContext.getPackageName(),
1599                     requiredSdkExtension,
1600                     wrapMigrationCallback(executor, callback));
1601 
1602         } catch (RemoteException e) {
1603             throw e.rethrowFromSystemServer();
1604         }
1605     }
1606 
1607     @SuppressWarnings("unchecked")
getReadCallback( @onNull Executor executor, @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback)1608     private <T extends Record> IReadRecordsResponseCallback.Stub getReadCallback(
1609             @NonNull Executor executor,
1610             @NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
1611         return new IReadRecordsResponseCallback.Stub() {
1612             @Override
1613             public void onResult(ReadRecordsResponseParcel parcel) {
1614                 Binder.clearCallingIdentity();
1615                 try {
1616                     List<T> externalRecords =
1617                             (List<T>)
1618                                     mInternalExternalRecordConverter.getExternalRecords(
1619                                             parcel.getRecordsParcel().getRecords());
1620                     executor.execute(
1621                             () ->
1622                                     callback.onResult(
1623                                             new ReadRecordsResponse<>(
1624                                                     externalRecords, parcel.getPageToken())));
1625                 } catch (ClassCastException castException) {
1626                     HealthConnectException healthConnectException =
1627                             new HealthConnectException(
1628                                     HealthConnectException.ERROR_INTERNAL,
1629                                     castException.getMessage());
1630                     returnError(
1631                             executor,
1632                             new HealthConnectExceptionParcel(healthConnectException),
1633                             callback);
1634                 }
1635             }
1636 
1637             @Override
1638             public void onError(HealthConnectExceptionParcel exception) {
1639                 returnError(executor, exception, callback);
1640             }
1641         };
1642     }
1643 
1644     private List<Record> getRecordsWithUids(List<Record> records, List<String> uids) {
1645         int i = 0;
1646         for (Record record : records) {
1647             record.getMetadata().setId(uids.get(i++));
1648         }
1649 
1650         return records;
1651     }
1652 
1653     private void returnError(
1654             Executor executor,
1655             HealthConnectExceptionParcel exception,
1656             OutcomeReceiver<?, HealthConnectException> callback) {
1657         Binder.clearCallingIdentity();
1658         executor.execute(() -> callback.onError(exception.getHealthConnectException()));
1659     }
1660 
1661     /** @hide */
1662     @Retention(RetentionPolicy.SOURCE)
1663     @IntDef({
1664         DATA_DOWNLOAD_STATE_UNKNOWN,
1665         DATA_DOWNLOAD_STARTED,
1666         DATA_DOWNLOAD_RETRY,
1667         DATA_DOWNLOAD_FAILED,
1668         DATA_DOWNLOAD_COMPLETE
1669     })
1670     public @interface DataDownloadState {}
1671 
1672     /**
1673      * Returns {@code true} if the given permission protects access to health connect data.
1674      *
1675      * @hide
1676      */
1677     @SystemApi
1678     public static boolean isHealthPermission(
1679             @NonNull Context context, @NonNull final String permission) {
1680         if (!permission.startsWith(HEALTH_PERMISSION_PREFIX)) {
1681             return false;
1682         }
1683         return getHealthPermissions(context).contains(permission);
1684     }
1685 
1686     /**
1687      * Returns an <b>immutable</b> set of health permissions defined within the module and belonging
1688      * to {@link android.health.connect.HealthPermissions#HEALTH_PERMISSION_GROUP}.
1689      *
1690      * <p><b>Note:</b> If we, for some reason, fail to retrieve these, we return an empty set rather
1691      * than crashing the device. This means the health permissions infra will be inactive.
1692      *
1693      * @hide
1694      */
1695     @NonNull
1696     @SystemApi
1697     public static Set<String> getHealthPermissions(@NonNull Context context) {
1698         if (sHealthPermissions != null) {
1699             return sHealthPermissions;
1700         }
1701 
1702         PackageInfo packageInfo;
1703         try {
1704             final PackageManager pm = context.getApplicationContext().getPackageManager();
1705             final PermissionGroupInfo permGroupInfo =
1706                     pm.getPermissionGroupInfo(
1707                             android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP,
1708                             /* flags= */ 0);
1709             packageInfo =
1710                     pm.getPackageInfo(
1711                             permGroupInfo.packageName,
1712                             PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
1713         } catch (PackageManager.NameNotFoundException ex) {
1714             Log.e(TAG, "Health permission group or HC package not found", ex);
1715             sHealthPermissions = Collections.emptySet();
1716             return sHealthPermissions;
1717         }
1718 
1719         Set<String> permissions = new HashSet<>();
1720         for (PermissionInfo perm : packageInfo.permissions) {
1721             if (android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP.equals(
1722                     perm.group)) {
1723                 permissions.add(perm.name);
1724             }
1725         }
1726         sHealthPermissions = Collections.unmodifiableSet(permissions);
1727         return sHealthPermissions;
1728     }
1729 
1730     @NonNull
1731     private static IMigrationCallback wrapMigrationCallback(
1732             @NonNull @CallbackExecutor Executor executor,
1733             @NonNull OutcomeReceiver<Void, MigrationException> callback) {
1734         return new IMigrationCallback.Stub() {
1735             @Override
1736             public void onSuccess() {
1737                 Binder.clearCallingIdentity();
1738                 executor.execute(() -> callback.onResult(null));
1739             }
1740 
1741             @Override
1742             public void onError(MigrationException exception) {
1743                 Binder.clearCallingIdentity();
1744                 executor.execute(() -> callback.onError(exception));
1745             }
1746         };
1747     }
1748 }
1749