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