blob: e41bae8a3563bd3f7f984d0c588d51a3198f450b [file] [log] [blame]
/*
* 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.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
import static android.health.connect.HealthPermissions.MANAGE_HEALTH_PERMISSIONS;
import android.Manifest;
import android.annotation.CallbackExecutor;
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.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.Record;
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.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 app’s 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;
private static final String TAG = "HealthConnectManager";
private static final String HEALTH_PERMISSION_PREFIX = "android.permission.health.";
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.
*
* @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.
*
* @hide
*/
@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
*/
@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 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::toRecordInternal).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(
getRecordsWithUids(
records, 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);
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);
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();
}
}
@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> getRecordsWithUids(List<Record> records, List<String> uids) {
int i = 0;
for (Record record : records) {
record.getMetadata().setId(uids.get(i++));
}
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));
}
};
}
}