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