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