script-astra/Android/Sdk/sources/android-35/android/health/connect/HealthConnectManager.java
localadmin 4380f00a78 init
2025-01-20 18:15:20 +03:00

2049 lines
86 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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.
*
* <p>HealthConnect is an offline, on-device storage that unifies data from multiple devices and
* apps into an ecosystem featuring.
*
* <ul>
* <li>APIs to insert data of various types into the system.
* </ul>
*
* <p>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 apps health permission rationale/data policy.
*
* <p><b>Note:</b> 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.
*
* <p>Shows a list of apps which request at least one permission of the Health permission group.
*
* <p>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.
*
* <p>Input: caller must provide `String` extra EXTRA_SESSION_ID
*
* <p>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}.
*
* <p>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}.
*
* <p>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.
*
* <p>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.
*
* <p>Input: caller must provide `String[]` extra [EXTRA_PERMISSIONS]
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p><b>Note:</b> 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.
*
* <p class="note">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}.
*
* <p class="note">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.
*
* <p>See also {@link #updateDataDownloadState}
*
* @hide
*/
@SystemApi public static final int DATA_DOWNLOAD_STATE_UNKNOWN = 0;
/**
* Indicates that the download has started.
*
* <p>See also {@link #updateDataDownloadState}
*
* @hide
*/
@SystemApi public static final int DATA_DOWNLOAD_STARTED = 1;
/**
* Indicates that the download is being retried.
*
* <p>See also {@link #updateDataDownloadState}
*
* @hide
*/
@SystemApi public static final int DATA_DOWNLOAD_RETRY = 2;
/**
* Indicates that the download has failed.
*
* <p>See also {@link #updateDataDownloadState}
*
* @hide
*/
@SystemApi public static final int DATA_DOWNLOAD_FAILED = 3;
/**
* Indicates that the download has completed.
*
* <p>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.
*
* <p>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.
*
* <p class="note">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<String> 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.
*
* <p><b>Note:</b> 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.
*
* <p><b>Note:</b> 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<String> 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.
*
* <p>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.
*
* <p>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<String, Integer> getHealthPermissionsFlags(
@NonNull String packageName, @NonNull List<String> 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<String> 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<Record> records,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> 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<RecordInternal<?>> 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 <T> Result type of the aggregation.
* <p>Note:
* <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
* typed in nature.
* <p>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 <T> void aggregate(
@NonNull AggregateRecordsRequest<T> request,
@NonNull @CallbackExecutor Executor executor,
@NonNull
OutcomeReceiver<AggregateRecordsResponse<T>, 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<T>)
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 <T> Result type of the aggregation.
* <p>Note:
* <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
* typed in nature.
* <p>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 <T> void aggregateGroupByDuration(
@NonNull AggregateRecordsRequest<T> request,
@NonNull Duration duration,
@NonNull @CallbackExecutor Executor executor,
@NonNull
OutcomeReceiver<
List<AggregateRecordsGroupedByDurationResponse<T>>,
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<AggregateRecordsGroupedByDurationResponse<T>> result =
new ArrayList<>();
for (AggregateRecordsGroupedByDurationResponse<?>
aggregateRecordsGroupedByDurationResponse :
parcel.getAggregateDataResponseGroupedByDuration()) {
result.add(
(AggregateRecordsGroupedByDurationResponse<T>)
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 <T> Result type of the aggregation.
* <p>Note:
* <p>This type is embedded in the {@link AggregationType} as {@link AggregationType} are
* typed in nature.
* <p>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 <T> void aggregateGroupByPeriod(
@NonNull AggregateRecordsRequest<T> request,
@NonNull Period period,
@NonNull @CallbackExecutor Executor executor,
@NonNull
OutcomeReceiver<
List<AggregateRecordsGroupedByPeriodResponse<T>>,
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<AggregateRecordsGroupedByPeriodResponse<T>> result =
new ArrayList<>();
for (AggregateRecordsGroupedByPeriodResponse<?>
aggregateRecordsGroupedByPeriodResponse :
parcel.getAggregateDataResponseGroupedByPeriod()) {
result.add(
(AggregateRecordsGroupedByPeriodResponse<T>)
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.
*
* <p>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<Void, HealthConnectException> 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}.
*
* <p>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<RecordIdFilter> recordIds,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Void, HealthConnectException> 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}.
*
* <p>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<? extends Record> recordType,
@NonNull TimeRangeFilter timeRangeFilter,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Void, HealthConnectException> 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<ChangeLogsResponse, HealthConnectException> 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.
*
* <p>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<ChangeLogTokenResponse, HealthConnectException> 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<FetchDataOriginsPriorityOrderResponse, HealthConnectException>
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<Void, HealthConnectException> 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<Class<? extends Record>, 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.
*
* <p>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.
*
* <p>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<Void, HealthConnectException> 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<List<AccessLog>, 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}
*
* <p>Number of records returned by this API will depend based on below factors:
*
* <p>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.
*
* <p>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.
*
* <p>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.
*
* <p>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 <T extends Record> void readRecords(
@NonNull ReadRecordsRequest<T> request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<ReadRecordsResponse<T>, 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}.
*
* <p>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<Record> records,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Void, HealthConnectException> callback) {
Objects.requireNonNull(records);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
try {
List<RecordInternal<?>> 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<ApplicationInfoResponse, HealthConnectException> 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.
*
* <p>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.
*
* <p>The file names passed should be the same as the ones on the original device that were
* backed up or are being transferred directly.
*
* <p>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.
*
* <p>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.
*
* <p>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<String, ParcelFileDescriptor> pfdsByFileName,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Void, StageRemoteDataException> 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.
*
* <p>The shared data must later be sent for Backup to cloud or another device.
*
* <p>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<String, ParcelFileDescriptor> 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<String> 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.
*
* <p>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.
*
* <p>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.
*
* <p>The only valid order of state transition are:
*
* <ul>
* <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_COMPLETE}
* <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
* #DATA_DOWNLOAD_COMPLETE}
* <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_FAILED}
* <li>{@link #DATA_DOWNLOAD_STARTED} to {@link #DATA_DOWNLOAD_RETRY} to {@link
* #DATA_DOWNLOAD_FAILED}
* </ul>
*
* <p>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.
*
* <p>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.
*
* <p>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<HealthConnectMigrationUiState, HealthConnectException>
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.
*
* <p>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<HealthConnectDataState, HealthConnectException> 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<Class<? extends Record>> recordTypes,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<List<LocalDate>, 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<Void, MigrationException> 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<Void, MigrationException> 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<MigrationEntity> entities,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Void, MigrationException> 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<Void, MigrationException> 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<ScheduledExportStatus, HealthConnectException> 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.
*
* <p>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<List<ExportImportDocumentProvider>, HealthConnectException>
callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
try {
mService.queryDocumentProviders(
mContext.getUser(),
new IQueryDocumentProvidersCallback.Stub() {
@Override
public void onResult(List<ExportImportDocumentProvider> 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 <T extends Record> IReadRecordsResponseCallback.Stub getReadCallback(
@NonNull Executor executor,
@NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
return new IReadRecordsResponseCallback.Stub() {
@Override
public void onResult(ReadRecordsResponseParcel parcel) {
Binder.clearCallingIdentity();
try {
List<T> externalRecords =
(List<T>)
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<Record> toExternalRecordsWithUuids(
List<RecordInternal<?>> recordInternals, List<String> uuids) {
int i = 0;
List<Record> 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<?, HealthConnectException> 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 <b>immutable</b> set of health permissions defined within the module and belonging
* to {@link android.health.connect.HealthPermissions#HEALTH_PERMISSION_GROUP}.
*
* <p><b>Note:</b> 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<String> 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<String> 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<Void, MigrationException> 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.
*
* <p>Number of resources returned by this API will depend based on below factors:
*
* <ul>
* <li>When an app with read permissions allowed for the requested IDs calls the API from
* background then it will be able to read only its own inserted medical resources and
* will not get medical resources inserted by other apps. This may be less than the
* requested size.
* <li>When an app with all read permissions allowed for the requested IDs calls the API from
* foreground then it will be able to read all the corresponding medical resources.
* <li>When an app with less read permissions allowed to cover all the requested IDs calls the
* API from foreground then it will be able to read only the medical resources it has read
* permissions for. This may be less than the requested size.
* <li>App with only write permission but no read permission allowed will be able to read only
* its own inserted medical resources both when in foreground or background. This may be
* less than the requested size.
* <li>An app without both read and write permissions will not be able to read any medical
* resources and the API will throw Security Exception.
* </ul>
*
* @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<MedicalIdFilter> ids,
@NonNull Executor executor,
@NonNull OutcomeReceiver<List<MedicalResource>, 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");
}
}