/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.health.connect; import static android.health.connect.Constants.DEFAULT_LONG; import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE; import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION; import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS; import static com.android.healthfitness.flags.Flags.FLAG_EXPORT_IMPORT; import static com.android.healthfitness.flags.Flags.FLAG_PERSONAL_HEALTH_RECORD; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.annotation.WorkerThread; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PermissionGroupInfo; import android.content.pm.PermissionInfo; import android.health.connect.accesslog.AccessLog; import android.health.connect.accesslog.AccessLogsResponseParcel; import android.health.connect.aidl.ActivityDatesRequestParcel; import android.health.connect.aidl.ActivityDatesResponseParcel; import android.health.connect.aidl.AggregateDataRequestParcel; import android.health.connect.aidl.AggregateDataResponseParcel; import android.health.connect.aidl.ApplicationInfoResponseParcel; import android.health.connect.aidl.DeleteUsingFiltersRequestParcel; import android.health.connect.aidl.GetPriorityResponseParcel; import android.health.connect.aidl.HealthConnectExceptionParcel; import android.health.connect.aidl.IAccessLogsResponseCallback; import android.health.connect.aidl.IActivityDatesResponseCallback; import android.health.connect.aidl.IAggregateRecordsResponseCallback; import android.health.connect.aidl.IApplicationInfoResponseCallback; import android.health.connect.aidl.IChangeLogsResponseCallback; import android.health.connect.aidl.IDataStagingFinishedCallback; import android.health.connect.aidl.IEmptyResponseCallback; import android.health.connect.aidl.IGetChangeLogTokenCallback; import android.health.connect.aidl.IGetHealthConnectDataStateCallback; import android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback; import android.health.connect.aidl.IGetPriorityResponseCallback; import android.health.connect.aidl.IHealthConnectService; import android.health.connect.aidl.IInsertRecordsResponseCallback; import android.health.connect.aidl.IMigrationCallback; import android.health.connect.aidl.IReadRecordsResponseCallback; import android.health.connect.aidl.IRecordTypeInfoResponseCallback; import android.health.connect.aidl.InsertRecordsResponseParcel; import android.health.connect.aidl.ReadRecordsResponseParcel; import android.health.connect.aidl.RecordIdFiltersParcel; import android.health.connect.aidl.RecordTypeInfoResponseParcel; import android.health.connect.aidl.RecordsParcel; import android.health.connect.aidl.UpdatePriorityRequestParcel; import android.health.connect.changelog.ChangeLogTokenRequest; import android.health.connect.changelog.ChangeLogTokenResponse; import android.health.connect.changelog.ChangeLogsRequest; import android.health.connect.changelog.ChangeLogsResponse; import android.health.connect.datatypes.AggregationType; import android.health.connect.datatypes.DataOrigin; import android.health.connect.datatypes.MedicalResource; import android.health.connect.datatypes.Record; import android.health.connect.exportimport.ExportImportDocumentProvider; import android.health.connect.exportimport.IQueryDocumentProvidersCallback; import android.health.connect.exportimport.IScheduledExportStatusCallback; import android.health.connect.exportimport.ScheduledExportSettings; import android.health.connect.exportimport.ScheduledExportStatus; import android.health.connect.internal.datatypes.RecordInternal; import android.health.connect.internal.datatypes.utils.InternalExternalRecordConverter; import android.health.connect.migration.HealthConnectMigrationUiState; import android.health.connect.migration.MigrationEntity; import android.health.connect.migration.MigrationEntityParcel; import android.health.connect.migration.MigrationException; import android.health.connect.restore.StageRemoteDataException; import android.health.connect.restore.StageRemoteDataRequest; import android.os.Binder; import android.os.OutcomeReceiver; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; import java.time.Period; import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.stream.Collectors; /** * This class provides APIs to interact with the centralized HealthConnect storage maintained by the * system. * *

HealthConnect is an offline, on-device storage that unifies data from multiple devices and * apps into an ecosystem featuring. * *

* *

The basic unit of data in HealthConnect is represented as a {@link Record} object, which is * the base class for all the other data types such as {@link * android.health.connect.datatypes.StepsRecord}. */ @SystemService(Context.HEALTHCONNECT_SERVICE) public class HealthConnectManager { /** * Used in conjunction with {@link android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} to * launch UI to show an app’s health permission rationale/data policy. * *

Note: Used by apps to define an intent filter in conjunction with {@link * android.content.Intent#ACTION_VIEW_PERMISSION_USAGE} that the HC UI can link out to. */ // We use intent.category prefix to be compatible with HealthPermissions strings definitions. @SdkConstant(SdkConstant.SdkConstantType.INTENT_CATEGORY) public static final String CATEGORY_HEALTH_PERMISSIONS = "android.intent.category.HEALTH_PERMISSIONS"; /** * Activity action: Launch UI to manage (e.g. grant/revoke) health permissions. * *

Shows a list of apps which request at least one permission of the Health permission group. * *

Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the * app requesting the action. Optional: Adding package name extras launches a UI to manager * (e.g. grant/revoke) for this app. */ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_MANAGE_HEALTH_PERMISSIONS = "android.health.connect.action.MANAGE_HEALTH_PERMISSIONS"; /** * Activity action: Launch UI to share the route associated with an exercise session. * *

Input: caller must provide `String` extra EXTRA_SESSION_ID * *

Result will be delivered via [Activity.onActivityResult] with `ExerciseRoute` * EXTRA_EXERCISE_ROUTE. */ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_REQUEST_EXERCISE_ROUTE = "android.health.connect.action.REQUEST_EXERCISE_ROUTE"; /** * A string ID of a session to be used with {@link #ACTION_REQUEST_EXERCISE_ROUTE}. * *

This is used to specify route of which exercise session we want to request. */ public static final String EXTRA_SESSION_ID = "android.health.connect.extra.SESSION_ID"; /** * An exercise route requested via {@link #ACTION_REQUEST_EXERCISE_ROUTE}. * *

This is returned for a successful request to access a route associated with an exercise * session. */ public static final String EXTRA_EXERCISE_ROUTE = "android.health.connect.extra.EXERCISE_ROUTE"; /** * Activity action: Launch UI to show and manage (e.g. grant/revoke) health permissions. * *

Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with the name of the * app requesting the action must be present. An app can open only its own page. * *

Input: caller must provide `String[]` extra [EXTRA_PERMISSIONS] * *

Result will be delivered via [Activity.onActivityResult] with `String[]` * [EXTRA_PERMISSIONS] and `int[]` [EXTRA_PERMISSION_GRANT_RESULTS], similar to * [Activity.onRequestPermissionsResult] * * @hide */ @SystemApi @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_REQUEST_HEALTH_PERMISSIONS = "android.health.connect.action.REQUEST_HEALTH_PERMISSIONS"; /** * Activity action: Launch UI to health connect home settings screen. * *

shows a list of recent apps that accessed (e.g. read/write) health data and allows the * user to access health permissions and health data. * * @hide */ @SystemApi @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_HEALTH_HOME_SETTINGS = "android.health.connect.action.HEALTH_HOME_SETTINGS"; /** * Activity action: Launch UI to show and manage (e.g. delete/export) health data. * *

shows a list of health data categories and actions to manage (e.g. delete/export) health * data. * * @hide */ @SystemApi @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_MANAGE_HEALTH_DATA = "android.health.connect.action.MANAGE_HEALTH_DATA"; /** * Activity action: Display information regarding migration - e.g. asking the user to take some * action (e.g. update the system) so that migration can take place. * *

Note: Callers of the migration APIs must handle this intent. * * @hide */ @SystemApi @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SHOW_MIGRATION_INFO = "android.health.connect.action.SHOW_MIGRATION_INFO"; /** * Broadcast Action: Health Connect is ready to accept migrated data. * *

This broadcast is explicitly sent to Health Connect migration aware * applications to prompt them to start/continue HC data migration. Migration aware applications * are those that both hold {@code android.permission.MIGRATE_HEALTH_CONNECT_DATA} and handle * {@code android.health.connect.action.SHOW_MIGRATION_INFO}. * *

This is a protected intent that can only be sent by the system. * * @hide */ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION) @SystemApi public static final String ACTION_HEALTH_CONNECT_MIGRATION_READY = "android.health.connect.action.HEALTH_CONNECT_MIGRATION_READY"; /** * Unknown download state considered to be the default download state. * *

See also {@link #updateDataDownloadState} * * @hide */ @SystemApi public static final int DATA_DOWNLOAD_STATE_UNKNOWN = 0; /** * Indicates that the download has started. * *

See also {@link #updateDataDownloadState} * * @hide */ @SystemApi public static final int DATA_DOWNLOAD_STARTED = 1; /** * Indicates that the download is being retried. * *

See also {@link #updateDataDownloadState} * * @hide */ @SystemApi public static final int DATA_DOWNLOAD_RETRY = 2; /** * Indicates that the download has failed. * *

See also {@link #updateDataDownloadState} * * @hide */ @SystemApi public static final int DATA_DOWNLOAD_FAILED = 3; /** * Indicates that the download has completed. * *

See also {@link HealthConnectManager#updateDataDownloadState} * * @hide */ @SystemApi public static final int DATA_DOWNLOAD_COMPLETE = 4; /** * Unknown error during the last data export. * * @hide */ @FlaggedApi(FLAG_EXPORT_IMPORT) public static final int DATA_EXPORT_ERROR_UNKNOWN = 0; /** * No error during the last data export. * * @hide */ @FlaggedApi(FLAG_EXPORT_IMPORT) public static final int DATA_EXPORT_ERROR_NONE = 1; /** * Indicates that the last export failed because we lost access to the export file location. * * @hide */ @FlaggedApi(FLAG_EXPORT_IMPORT) public static final int DATA_EXPORT_LOST_FILE_ACCESS = 2; /** * Activity action: Launch activity exported by client application that handles onboarding to * Health Connect. * *

Health Connect will invoke this intent whenever the user attempts to connect an app that * has exported an activity that responds to this intent. The launched activity is responsible * for making permission requests and any other prerequisites for connecting to Health Connect. * *

Applications exporting an activity that is launched by this intent must also * guard it with {@link HealthPermissions#START_ONBOARDING} so that only the system can launch * it. * * @hide */ @SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION) public static final String ACTION_SHOW_ONBOARDING = "android.health.connect.action.SHOW_ONBOARDING"; private static final String TAG = "HealthConnectManager"; private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health."; @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression private static volatile Set sHealthPermissions; private final Context mContext; private final IHealthConnectService mService; private final InternalExternalRecordConverter mInternalExternalRecordConverter; /** @hide */ HealthConnectManager(@NonNull Context context, @NonNull IHealthConnectService service) { mContext = context; mService = service; mInternalExternalRecordConverter = InternalExternalRecordConverter.getInstance(); } /** * Grant a runtime permission to an application which the application does not already have. The * permission must have been requested by the application. If the application is not allowed to * hold the permission, a {@link java.lang.SecurityException} is thrown. If the package or * permission is invalid, a {@link java.lang.IllegalArgumentException} is thrown. * *

Note: This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET}. * * @hide */ @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) @UserHandleAware public void grantHealthPermission(@NonNull String packageName, @NonNull String permissionName) { try { mService.grantHealthPermission(packageName, permissionName, mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Revoke a health permission that was previously granted by {@link * #grantHealthPermission(String, String)} The permission must have been requested by the * application. If the application is not allowed to hold the permission, a {@link * java.lang.SecurityException} is thrown. If the package or permission is invalid, a {@link * java.lang.IllegalArgumentException} is thrown. * *

Note: This API sets {@code PackageManager.FLAG_PERMISSION_USER_SET} or {@code * PackageManager.FLAG_PERMISSION_USER_FIXED} based on the number of revocations of a particular * permission for a package. * * @hide */ @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) @UserHandleAware public void revokeHealthPermission( @NonNull String packageName, @NonNull String permissionName, @Nullable String reason) { try { mService.revokeHealthPermission( packageName, permissionName, reason, mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Revokes all health permissions that were previously granted by {@link * #grantHealthPermission(String, String)} If the package is invalid, a {@link * java.lang.IllegalArgumentException} is thrown. * * @hide */ @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) @UserHandleAware public void revokeAllHealthPermissions(@NonNull String packageName, @Nullable String reason) { try { mService.revokeAllHealthPermissions(packageName, reason, mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns a list of health permissions that were previously granted by {@link * #grantHealthPermission(String, String)}. * * @hide */ @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) @UserHandleAware public List getGrantedHealthPermissions(@NonNull String packageName) { try { return mService.getGrantedHealthPermissions(packageName, mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns permission flags for the given package name and Health permissions. * *

This is equivalent to calling {@link PackageManager#getPermissionFlags(String, String, * UserHandle)} for each provided permission except it throws an exception for non-Health or * undeclared permissions. Flag masks listed in {@link PackageManager#MASK_PERMISSION_FLAGS_ALL} * can be used to check the flag values. * *

Returned flags for invalid, non-Health or undeclared permissions are equal to zero. * * @return a map which contains all requested permissions as keys and corresponding flags as * values. * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not * Health permissions or not declared by the app. * @throws NullPointerException if any of the arguments is {@code null}. * @throws SecurityException if the caller doesn't possess {@code * android.permission.MANAGE_HEALTH_PERMISSIONS}. * @hide */ @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) @UserHandleAware public Map getHealthPermissionsFlags( @NonNull String packageName, @NonNull List permissions) { try { return mService.getHealthPermissionsFlags(packageName, mContext.getUser(), permissions); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Sets/clears {@link PackageManager#FLAG_PERMISSION_USER_FIXED} for given health permissions. * * @param value whether to set or clear the flag, {@code true} means set, {@code false} - clear. * @throws IllegalArgumentException if the package doesn't exist, any of the permissions are not * Health permissions or not declared by the app. * @throws NullPointerException if any of the arguments is {@code null}. * @throws SecurityException if the caller doesn't possess {@code * android.permission.MANAGE_HEALTH_PERMISSIONS}. * @hide */ @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) @UserHandleAware public void setHealthPermissionsUserFixedFlagValue( @NonNull String packageName, @NonNull List permissions, boolean value) { try { mService.setHealthPermissionsUserFixedFlagValue( packageName, mContext.getUser(), permissions, value); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the date from which an app have access to the historical health data. Returns null if * the package doesn't have historical access date. * * @hide */ @RequiresPermission(HealthPermissions.MANAGE_HEALTH_PERMISSIONS) @UserHandleAware @Nullable public Instant getHealthDataHistoricalAccessStartDate(@NonNull String packageName) { try { long dateMilli = mService.getHistoricalAccessStartDateInMilliseconds( packageName, mContext.getUser()); if (dateMilli == DEFAULT_LONG) { return null; } else { return Instant.ofEpochMilli(dateMilli); } } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Inserts {@code records} into the HealthConnect database. The records returned in {@link * InsertRecordsResponse} contains the unique IDs of the input records. The values are in same * order as {@code records}. In case of an error or a permission failure the HealthConnect * service, {@link OutcomeReceiver#onError} will be invoked with a {@link * HealthConnectException}. * * @param records list of records to be inserted. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws RuntimeException for internal errors */ public void insertRecords( @NonNull List records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(records); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { // Unset any set ids for insert. This is to prevent random string ids from creating // illegal argument exception. records.forEach((record) -> record.getMetadata().setId("")); List> recordInternals = records.stream() .map( record -> record.toRecordInternal() .setPackageName(mContext.getPackageName())) .collect(Collectors.toList()); mService.insertRecords( mContext.getAttributionSource(), new RecordsParcel(recordInternals), new IInsertRecordsResponseCallback.Stub() { @Override public void onResult(InsertRecordsResponseParcel parcel) { Binder.clearCallingIdentity(); executor.execute( () -> callback.onResult( new InsertRecordsResponse( toExternalRecordsWithUuids( recordInternals, parcel.getUids())))); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Get aggregations corresponding to {@code request}. * * @param Result type of the aggregation. *

Note: *

This type is embedded in the {@link AggregationType} as {@link AggregationType} are * typed in nature. *

Only {@link AggregationType}s that are of same type T can be queried together * @param request request for different aggregation. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @see AggregateRecordsResponse#get */ @NonNull @SuppressWarnings("unchecked") public void aggregate( @NonNull AggregateRecordsRequest request, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.aggregateRecords( mContext.getAttributionSource(), new AggregateDataRequestParcel(request), new IAggregateRecordsResponseCallback.Stub() { @Override public void onResult(AggregateDataResponseParcel parcel) { Binder.clearCallingIdentity(); try { executor.execute( () -> callback.onResult( (AggregateRecordsResponse) parcel.getAggregateDataResponse())); } catch (Exception exception) { callback.onError( new HealthConnectException( HealthConnectException.ERROR_INTERNAL)); } } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (ClassCastException classCastException) { returnError( executor, new HealthConnectExceptionParcel( new HealthConnectException(HealthConnectException.ERROR_INTERNAL)), callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped * by concrete intervals of time, for example 5 Hrs, 10 Hrs etc. * * @param Result type of the aggregation. *

Note: *

This type is embedded in the {@link AggregationType} as {@link AggregationType} are * typed in nature. *

Only {@link AggregationType}s that are of same type T can be queried together * @param request request for different aggregation. * @param duration Duration on which to group by results * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @see HealthConnectManager#aggregateGroupByPeriod */ @SuppressWarnings("unchecked") public void aggregateGroupByDuration( @NonNull AggregateRecordsRequest request, @NonNull Duration duration, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List>, HealthConnectException> callback) { Objects.requireNonNull(request); Objects.requireNonNull(duration); if (duration.toMillis() < 1) { throw new IllegalArgumentException("Duration should be at least 1 millisecond"); } Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.aggregateRecords( mContext.getAttributionSource(), new AggregateDataRequestParcel(request, duration), new IAggregateRecordsResponseCallback.Stub() { @Override public void onResult(AggregateDataResponseParcel parcel) { Binder.clearCallingIdentity(); List> result = new ArrayList<>(); for (AggregateRecordsGroupedByDurationResponse aggregateRecordsGroupedByDurationResponse : parcel.getAggregateDataResponseGroupedByDuration()) { result.add( (AggregateRecordsGroupedByDurationResponse) aggregateRecordsGroupedByDurationResponse); } executor.execute(() -> callback.onResult(result)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (ClassCastException classCastException) { returnError( executor, new HealthConnectExceptionParcel( new HealthConnectException(HealthConnectException.ERROR_INTERNAL)), callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Get aggregations corresponding to {@code request}. Use this API if results are to be grouped * by number of days. This API handles changes in {@link ZoneOffset} when computing the data on * a per-day basis. * * @param Result type of the aggregation. *

Note: *

This type is embedded in the {@link AggregationType} as {@link AggregationType} are * typed in nature. *

Only {@link AggregationType}s that are of same type T can be queried together * @param request Request for different aggregation. * @param period Period on which to group by results * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @see AggregateRecordsGroupedByPeriodResponse#get * @see HealthConnectManager#aggregateGroupByDuration */ @SuppressWarnings("unchecked") public void aggregateGroupByPeriod( @NonNull AggregateRecordsRequest request, @NonNull Period period, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< List>, HealthConnectException> callback) { Objects.requireNonNull(request); Objects.requireNonNull(period); if (period == Period.ZERO) { throw new IllegalArgumentException("Period duration should be at least a day"); } Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.aggregateRecords( mContext.getAttributionSource(), new AggregateDataRequestParcel(request, period), new IAggregateRecordsResponseCallback.Stub() { @Override public void onResult(AggregateDataResponseParcel parcel) { Binder.clearCallingIdentity(); List> result = new ArrayList<>(); for (AggregateRecordsGroupedByPeriodResponse aggregateRecordsGroupedByPeriodResponse : parcel.getAggregateDataResponseGroupedByPeriod()) { result.add( (AggregateRecordsGroupedByPeriodResponse) aggregateRecordsGroupedByPeriodResponse); } executor.execute(() -> callback.onResult(result)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (ClassCastException classCastException) { returnError( executor, new HealthConnectExceptionParcel( new HealthConnectException(HealthConnectException.ERROR_INTERNAL)), callback); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Deletes records based on the {@link DeleteUsingFiltersRequest}. This is only to be used by * health connect controller APK(s). Ids that don't exist will be ignored. * *

Deletions are performed in a transaction i.e. either all will be deleted or none * * @param request Request based on which to perform delete operation * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_PERMISSIONS) public void deleteRecords( @NonNull DeleteUsingFiltersRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.deleteUsingFilters( mContext.getAttributionSource(), new DeleteUsingFiltersRequestParcel(request), new IEmptyResponseCallback.Stub() { @Override public void onResult() { executor.execute(() -> callback.onResult(null)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException remoteException) { remoteException.rethrowFromSystemServer(); } } /** * Deletes records based on {@link RecordIdFilter}. * *

Deletions are performed in a transaction i.e. either all will be deleted or none * * @param recordIds recordIds on which to perform delete operation. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws IllegalArgumentException if {@code recordIds is empty} */ public void deleteRecords( @NonNull List recordIds, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(recordIds); Objects.requireNonNull(executor); Objects.requireNonNull(callback); if (recordIds.isEmpty()) { throw new IllegalArgumentException("record ids can't be empty"); } try { mService.deleteUsingFiltersForSelf( mContext.getAttributionSource(), new DeleteUsingFiltersRequestParcel( new RecordIdFiltersParcel(recordIds), mContext.getPackageName()), new IEmptyResponseCallback.Stub() { @Override public void onResult() { executor.execute(() -> callback.onResult(null)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException remoteException) { remoteException.rethrowFromSystemServer(); } } /** * Deletes records based on the {@link TimeRangeFilter}. * *

Deletions are performed in a transaction i.e. either all will be deleted or none * * @param recordType recordType to perform delete operation on. * @param timeRangeFilter time filter based on which to delete the records. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. */ public void deleteRecords( @NonNull Class recordType, @NonNull TimeRangeFilter timeRangeFilter, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(recordType); Objects.requireNonNull(timeRangeFilter); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.deleteUsingFiltersForSelf( mContext.getAttributionSource(), new DeleteUsingFiltersRequestParcel( new DeleteUsingFiltersRequest.Builder() .addDataOrigin( new DataOrigin.Builder() .setPackageName(mContext.getPackageName()) .build()) .addRecordType(recordType) .setTimeRangeFilter(timeRangeFilter) .build()), new IEmptyResponseCallback.Stub() { @Override public void onResult() { executor.execute(() -> callback.onResult(null)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException remoteException) { remoteException.rethrowFromSystemServer(); } } /** * Get change logs post the time when {@code token} was generated. * * @param changeLogsRequest The token from {@link HealthConnectManager#getChangeLogToken}. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @see HealthConnectManager#getChangeLogToken */ public void getChangeLogs( @NonNull ChangeLogsRequest changeLogsRequest, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(changeLogsRequest); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.getChangeLogs( mContext.getAttributionSource(), changeLogsRequest, new IChangeLogsResponseCallback.Stub() { @Override public void onResult(ChangeLogsResponse parcel) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(parcel)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (ClassCastException invalidArgumentException) { callback.onError( new HealthConnectException( HealthConnectException.ERROR_INVALID_ARGUMENT, invalidArgumentException.getMessage())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Get token for {HealthConnectManager#getChangeLogs}. Changelogs requested corresponding to * this token will be post the time this token was generated by the system all items that match * the given filters. * *

Tokens from this request are to be passed to {HealthConnectManager#getChangeLogs} * * @param request A request to get changelog token * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. */ public void getChangeLogToken( @NonNull ChangeLogTokenRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { try { mService.getChangeLogToken( mContext.getAttributionSource(), request, new IGetChangeLogTokenCallback.Stub() { @Override public void onResult(ChangeLogTokenResponse parcel) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(parcel)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Fetch the data priority order of the contributing {@link DataOrigin} for {@code * dataCategory}. * * @param dataCategory {@link HealthDataCategory} for which to get the priority order * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void fetchDataOriginsPriorityOrder( @HealthDataCategory.Type int dataCategory, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { try { mService.getCurrentPriority( mContext.getPackageName(), dataCategory, new IGetPriorityResponseCallback.Stub() { @Override public void onResult(GetPriorityResponseParcel response) { Binder.clearCallingIdentity(); executor.execute( () -> callback.onResult(response.getPriorityResponse())); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Updates the priority order of the apps as per {@code request} * * @param request new priority order update request * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void updateDataOriginPriorityOrder( @NonNull UpdateDataOriginPriorityOrderRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { try { mService.updatePriority( mContext.getPackageName(), new UpdatePriorityRequestParcel(request), new IEmptyResponseCallback.Stub() { @Override public void onResult() { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(null)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Retrieves {@link RecordTypeInfoResponse} for each RecordType. * * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void queryAllRecordTypesInfo( @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver< Map, RecordTypeInfoResponse>, HealthConnectException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.queryAllRecordTypesInfo( new IRecordTypeInfoResponseCallback.Stub() { @Override public void onResult(RecordTypeInfoResponseParcel parcel) { Binder.clearCallingIdentity(); executor.execute( () -> callback.onResult(parcel.getRecordTypeInfoResponses())); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns currently set auto delete period for this user. * *

If you are calling this function for the first time after a user unlock, this might take * some time so consider calling this on a thread. * * @return Auto delete period in days, 0 is returned if auto delete period is not set. * @throws RuntimeException for internal errors * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) @IntRange(from = 0, to = 7300) public int getRecordRetentionPeriodInDays() { try { return mService.getRecordRetentionPeriodInDays(mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Sets auto delete period (for all the records to be automatically deleted) for this user. * *

Note: The max value of auto delete period can be 7300 i.e. ~20 years * * @param days Auto period to be set in days. Use 0 to unset this value. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws RuntimeException for internal errors * @throws IllegalArgumentException if {@code days} is not between 0 and 7300 * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void setRecordRetentionPeriodInDays( @IntRange(from = 0, to = 7300) int days, @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); if (days < 0 || days > 7300) { throw new IllegalArgumentException("days should be between " + 0 + " and " + 7300); } try { mService.setRecordRetentionPeriodInDays( days, mContext.getUser(), new IEmptyResponseCallback.Stub() { @Override public void onResult() { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(null)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Returns a list of access logs with package name and its access time for each record type. * * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void queryAccessLogs( @NonNull Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.queryAccessLogs( mContext.getPackageName(), new IAccessLogsResponseCallback.Stub() { @Override public void onResult(AccessLogsResponseParcel parcel) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(parcel.getAccessLogs())); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * API to read records based on {@link ReadRecordsRequestUsingFilters} or {@link * ReadRecordsRequestUsingIds} * *

Number of records returned by this API will depend based on below factors: * *

When an app with read permission allowed calls the API from background then it will be * able to read only its own inserted records and will not get records inserted by other apps. * This may be less than the total records present for the record type. * *

When an app with read permission allowed calls the API from foreground then it will be * able to read all records for the record type. * *

App with only write permission but no read permission allowed will be able to read only * its own inserted records both when in foreground or background. * *

An app without both read and write permissions will not be able to read any record and the * API will throw Security Exception. * * @param request Read request based on {@link ReadRecordsRequestUsingFilters} or {@link * ReadRecordsRequestUsingIds} * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws IllegalArgumentException if request page size set is more than 5000 in {@link * ReadRecordsRequestUsingFilters} * @throws SecurityException if app without read or write permission tries to read. */ public void readRecords( @NonNull ReadRecordsRequest request, @NonNull Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { Objects.requireNonNull(request); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.readRecords( mContext.getAttributionSource(), request.toReadRecordsRequestParcel(), getReadCallback(executor, callback)); } catch (RemoteException remoteException) { remoteException.rethrowFromSystemServer(); } } /** * Updates {@code records} into the HealthConnect database. In case of an error or a permission * failure the HealthConnect service, {@link OutcomeReceiver#onError} will be invoked with a * {@link HealthConnectException}. * *

In case the input record to be updated does not exist in the database or the caller is not * the owner of the record then {@link HealthConnectException#ERROR_INVALID_ARGUMENT} will be * thrown. * * @param records list of records to be updated. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws IllegalArgumentException if at least one of the records is missing both * ClientRecordID and UUID. */ public void updateRecords( @NonNull List records, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(records); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { List> recordInternals = records.stream().map(Record::toRecordInternal).collect(Collectors.toList()); // Verify if the input record has clientRecordId or UUID. for (RecordInternal recordInternal : recordInternals) { if ((recordInternal.getClientRecordId() == null || recordInternal.getClientRecordId().isEmpty()) && recordInternal.getUuid() == null) { throw new IllegalArgumentException( "At least one of the records is missing both ClientRecordID" + " and UUID. RecordType of the input: " + recordInternal.getRecordType()); } } mService.updateRecords( mContext.getAttributionSource(), new RecordsParcel(recordInternals), new IEmptyResponseCallback.Stub() { @Override public void onResult() { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(null)); } @Override public void onError(HealthConnectExceptionParcel exception) { Binder.clearCallingIdentity(); callback.onError(exception.getHealthConnectException()); } }); } catch (ArithmeticException | ClassCastException | IllegalArgumentException invalidArgumentException) { throw new IllegalArgumentException(invalidArgumentException); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns information, represented by {@code ApplicationInfoResponse}, for all the packages * that have contributed to the health connect DB. If the application is does not have * permissions to query other packages, a {@link java.lang.SecurityException} is thrown. * * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @NonNull @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void getContributorApplicationsInfo( @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.getContributorApplicationsInfo( new IApplicationInfoResponseCallback.Stub() { @Override public void onResult(ApplicationInfoResponseParcel parcel) { Binder.clearCallingIdentity(); executor.execute( () -> callback.onResult( new ApplicationInfoResponse( parcel.getAppInfoList()))); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Stages all HealthConnect remote data and returns any errors in a callback. Errors encountered * for all the files are shared in the provided callback. Any authorization / permissions * related error is reported to the callback with an empty file name. * *

The staged data will later be restored (integrated) into the existing Health Connect data. * Any existing data will not be affected by the staged data. * *

The file names passed should be the same as the ones on the original device that were * backed up or are being transferred directly. * *

If a file already exists in the staged data then it will be replaced. However, note that * staging data is a one time process. And if the staged data has already been processed then * any attempt to stage data again will be silently ignored. * *

The caller is responsible for closing the original file descriptors. The file descriptors * are duplicated and the originals may be closed by the application at any time after this API * returns. * *

The caller should update the data download states using {@link #updateDataDownloadState} * before calling this API. * * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s. * @param executor The {@link Executor} on which to invoke the callback. * @param callback The callback which will receive the outcome of this call. * @hide */ @SystemApi @UserHandleAware @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA) public void stageAllHealthConnectRemoteData( @NonNull Map pfdsByFileName, @NonNull Executor executor, @NonNull OutcomeReceiver callback) throws NullPointerException { Objects.requireNonNull(pfdsByFileName); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.stageAllHealthConnectRemoteData( new StageRemoteDataRequest(pfdsByFileName), mContext.getUser(), new IDataStagingFinishedCallback.Stub() { @Override public void onResult() { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(null)); } @Override public void onError(StageRemoteDataException stageRemoteDataException) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onError(stageRemoteDataException)); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Copies all HealthConnect backup data in the passed FDs. * *

The shared data must later be sent for Backup to cloud or another device. * *

We are responsible for closing the original file descriptors. The caller must not close * the FD before that. * * @param pfdsByFileName The map of file names and their {@link ParcelFileDescriptor}s. * @hide */ public void getAllDataForBackup(@NonNull Map pfdsByFileName) { Objects.requireNonNull(pfdsByFileName); try { mService.getAllDataForBackup( new StageRemoteDataRequest(pfdsByFileName), mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns the names of all HealthConnect Backup files * * @hide */ public Set getAllBackupFileNames(boolean forDeviceToDevice) { try { return mService.getAllBackupFileNames(forDeviceToDevice).getFileNames(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Deletes all previously staged HealthConnect data from the disk. For testing purposes only. * *

This deletes only the staged data leaving any other Health Connect data untouched. * * @hide */ @TestApi @UserHandleAware public void deleteAllStagedRemoteData() throws NullPointerException { try { mService.deleteAllStagedRemoteData(mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Updates the download state of the Health Connect data. * *

The data should've been downloaded and the corresponding download states updated before * the app calls {@link #stageAllHealthConnectRemoteData}. Once {@link * #stageAllHealthConnectRemoteData} has been called the downloaded state becomes {@link * #DATA_DOWNLOAD_COMPLETE} and future attempts to update the download state are ignored. * *

The only valid order of state transition are: * *

* *

Note that it's okay if some states are missing in of the sequences above but the order has * to be one of the above. * *

Only one app will have the permission to call this API so it is assured that no one else * will be able to update this state. * * @param downloadState The download state which needs to be purely from {@link * DataDownloadState} * @hide */ @SystemApi @UserHandleAware @RequiresPermission(Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA) public void updateDataDownloadState(@DataDownloadState int downloadState) { try { mService.updateDataDownloadState(downloadState); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Asynchronously returns the current UI state of Health Connect as it goes through the * Data-Migration process. In case there was an error reading the data on the disk the error * will be returned in the callback. * *

See also {@link HealthConnectMigrationUiState} object describing the HealthConnect UI * state. * * @param executor The {@link Executor} on which to invoke the callback. * @param callback The callback which will receive the current {@link * HealthConnectMigrationUiState} or the {@link HealthConnectException}. * @hide */ @UserHandleAware @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) @NonNull public void getHealthConnectMigrationUiState( @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.getHealthConnectMigrationUiState( new IGetHealthConnectMigrationUiStateCallback.Stub() { @Override public void onResult(HealthConnectMigrationUiState migrationUiState) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(migrationUiState)); } @Override public void onError(HealthConnectExceptionParcel exception) { Binder.clearCallingIdentity(); executor.execute( () -> callback.onError(exception.getHealthConnectException())); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Asynchronously returns the current state of the Health Connect data as it goes through the * Data-Restore and/or the Data-Migration process. In case there was an error reading the data * on the disk the error will be returned in the callback. * *

See also {@link HealthConnectDataState} object describing the HealthConnect state. * * @param executor The {@link Executor} on which to invoke the callback. * @param callback The callback which will receive the current {@link HealthConnectDataState} or * the {@link HealthConnectException}. * @hide */ @SystemApi @UserHandleAware @RequiresPermission( anyOf = { MANAGE_HEALTH_DATA_PERMISSION, Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA }) @NonNull public void getHealthConnectDataState( @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.getHealthConnectDataState( new IGetHealthConnectDataStateCallback.Stub() { @Override public void onResult(HealthConnectDataState healthConnectDataState) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(healthConnectDataState)); } @Override public void onError(HealthConnectExceptionParcel exception) { Binder.clearCallingIdentity(); executor.execute( () -> callback.onError(exception.getHealthConnectException())); } }); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Returns a list of unique dates for which the DB has at least one entry. * * @param recordTypes List of record types classes for which to get the activity dates. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws java.lang.IllegalArgumentException If the record types list is empty. * @hide */ @NonNull @SystemApi @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void queryActivityDates( @NonNull List> recordTypes, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); Objects.requireNonNull(recordTypes); if (recordTypes.isEmpty()) { throw new IllegalArgumentException("Record types list can not be empty"); } try { mService.getActivityDates( new ActivityDatesRequestParcel(recordTypes), new IActivityDatesResponseCallback.Stub() { @Override public void onResult(ActivityDatesResponseParcel parcel) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(parcel.getDates())); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException exception) { exception.rethrowFromSystemServer(); } } /** * Marks the start of the migration and block API calls. * * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA) @SystemApi public void startMigration( @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.startMigration( mContext.getPackageName(), wrapMigrationCallback(executor, callback)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Marks the end of the migration. * * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA) @SystemApi public void finishMigration( @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.finishMigration( mContext.getPackageName(), wrapMigrationCallback(executor, callback)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Writes data to the module database. * * @param entities List of {@link MigrationEntity} to migrate. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA) @SystemApi public void writeMigrationData( @NonNull List entities, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(entities); Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.writeMigrationData( mContext.getPackageName(), new MigrationEntityParcel(entities), wrapMigrationCallback(executor, callback)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Sets the minimum version on which the module will inform the migrator package of its * migration readiness. * * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @hide */ @SystemApi @RequiresPermission(Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA) public void insertMinDataMigrationSdkExtensionVersion( int requiredSdkExtension, @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.insertMinDataMigrationSdkExtensionVersion( mContext.getPackageName(), requiredSdkExtension, wrapMigrationCallback(executor, callback)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({DATA_EXPORT_ERROR_UNKNOWN, DATA_EXPORT_ERROR_NONE, DATA_EXPORT_LOST_FILE_ACCESS}) public @interface DataExportError {} /** * Configures the settings for the scheduled export of Health Connect data. * * @param settings Settings to use for the scheduled export. Use null to clear the settings. * @throws RuntimeException for internal errors * @hide */ @SuppressWarnings("NullAway") // TODO: b/178748627 - fix this suppression. @WorkerThread @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void configureScheduledExport(@Nullable ScheduledExportSettings settings) { try { mService.configureScheduledExport(settings, mContext.getUser()); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Queries the document providers available to be used for export/import. * * @throws RuntimeException for internal errors * @hide */ @FlaggedApi(FLAG_EXPORT_IMPORT) @WorkerThread @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void getScheduledExportStatus( @NonNull Executor executor, @NonNull OutcomeReceiver callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.getScheduledExportStatus( mContext.getUser(), new IScheduledExportStatusCallback.Stub() { @Override public void onResult(ScheduledExportStatus status) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(status)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } /** * Returns currently set period between scheduled exports for this user. * *

If you are calling this function for the first time after a user unlock, this might take * some time so consider calling this on a thread. * * @return Period between scheduled exports in days, 0 is returned if period between scheduled * exports is not set. * @throws RuntimeException for internal errors * @hide */ @WorkerThread @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) @IntRange(from = 0, to = 30) public int getScheduledExportPeriodInDays() { try { return mService.getScheduledExportPeriodInDays(mContext.getUser()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } /** * Queries the document providers available to be used for export/import. * * @throws RuntimeException for internal errors * @hide */ @FlaggedApi(FLAG_EXPORT_IMPORT) @WorkerThread @RequiresPermission(MANAGE_HEALTH_DATA_PERMISSION) public void queryDocumentProviders( @NonNull Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); try { mService.queryDocumentProviders( mContext.getUser(), new IQueryDocumentProvidersCallback.Stub() { @Override public void onResult(List providers) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(providers)); } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }); } catch (RemoteException e) { e.rethrowFromSystemServer(); } } @SuppressWarnings("unchecked") private IReadRecordsResponseCallback.Stub getReadCallback( @NonNull Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { return new IReadRecordsResponseCallback.Stub() { @Override public void onResult(ReadRecordsResponseParcel parcel) { Binder.clearCallingIdentity(); try { List externalRecords = (List) mInternalExternalRecordConverter.getExternalRecords( parcel.getRecordsParcel().getRecords()); executor.execute( () -> callback.onResult( new ReadRecordsResponse<>( externalRecords, parcel.getPageToken()))); } catch (ClassCastException castException) { HealthConnectException healthConnectException = new HealthConnectException( HealthConnectException.ERROR_INTERNAL, castException.getMessage()); returnError( executor, new HealthConnectExceptionParcel(healthConnectException), callback); } } @Override public void onError(HealthConnectExceptionParcel exception) { returnError(executor, exception, callback); } }; } private List toExternalRecordsWithUuids( List> recordInternals, List uuids) { int i = 0; List records = new ArrayList<>(); for (RecordInternal recordInternal : recordInternals) { recordInternal.setUuid(uuids.get(i++)); records.add(recordInternal.toExternalRecord()); } return records; } private void returnError( Executor executor, HealthConnectExceptionParcel exception, OutcomeReceiver callback) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onError(exception.getHealthConnectException())); } /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef({ DATA_DOWNLOAD_STATE_UNKNOWN, DATA_DOWNLOAD_STARTED, DATA_DOWNLOAD_RETRY, DATA_DOWNLOAD_FAILED, DATA_DOWNLOAD_COMPLETE }) public @interface DataDownloadState {} /** * Returns {@code true} if the given permission protects access to health connect data. * * @hide */ @SystemApi public static boolean isHealthPermission( @NonNull Context context, @NonNull final String permission) { if (!permission.startsWith(HEALTH_PERMISSION_PREFIX)) { return false; } return getHealthPermissions(context).contains(permission); } /** * Returns an immutable set of health permissions defined within the module and belonging * to {@link android.health.connect.HealthPermissions#HEALTH_PERMISSION_GROUP}. * *

Note: If we, for some reason, fail to retrieve these, we return an empty set rather * than crashing the device. This means the health permissions infra will be inactive. * * @hide */ @NonNull @SystemApi public static Set getHealthPermissions(@NonNull Context context) { if (sHealthPermissions != null) { return sHealthPermissions; } PackageInfo packageInfo; try { final PackageManager pm = context.getApplicationContext().getPackageManager(); final PermissionGroupInfo permGroupInfo = pm.getPermissionGroupInfo( android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP, /* flags= */ 0); packageInfo = pm.getPackageInfo( permGroupInfo.packageName, PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS)); } catch (PackageManager.NameNotFoundException ex) { Log.e(TAG, "Health permission group or HC package not found", ex); sHealthPermissions = Collections.emptySet(); return sHealthPermissions; } Set permissions = new HashSet<>(); for (PermissionInfo perm : packageInfo.permissions) { if (android.health.connect.HealthPermissions.HEALTH_PERMISSION_GROUP.equals( perm.group)) { permissions.add(perm.name); } } sHealthPermissions = Collections.unmodifiableSet(permissions); return sHealthPermissions; } @NonNull private static IMigrationCallback wrapMigrationCallback( @NonNull @CallbackExecutor Executor executor, @NonNull OutcomeReceiver callback) { return new IMigrationCallback.Stub() { @Override public void onSuccess() { Binder.clearCallingIdentity(); executor.execute(() -> callback.onResult(null)); } @Override public void onError(MigrationException exception) { Binder.clearCallingIdentity(); executor.execute(() -> callback.onError(exception)); } }; } /** * Reads {@link MedicalResource}s based on a list of {@link MedicalIdFilter}s. * *

Number of resources returned by this API will depend based on below factors: * *

* * @param ids Identifiers on which to perform read operation. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive result of performing this operation. * @throws IllegalArgumentException if {@code ids} is empty or its size is more than 5000. */ @FlaggedApi(FLAG_PERSONAL_HEALTH_RECORD) public void readMedicalResources( @NonNull List ids, @NonNull Executor executor, @NonNull OutcomeReceiver, HealthConnectException> callback) { Objects.requireNonNull(ids); Objects.requireNonNull(executor); Objects.requireNonNull(callback); if (ids.size() >= MAXIMUM_PAGE_SIZE) { throw new IllegalArgumentException("Maximum allowed pageSize is " + MAXIMUM_PAGE_SIZE); } throw new UnsupportedOperationException("Not implemented"); } }