blob: c25a4e2a7755f0b4e6301943df84746ba0539d11 [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.healthconnect;
import static android.Manifest.permission.QUERY_ALL_PACKAGES;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.annotation.CallbackExecutor;
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.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.healthconnect.aidl.AggregateDataRequestParcel;
import android.healthconnect.aidl.AggregateDataResponseParcel;
import android.healthconnect.aidl.ApplicationInfoResponseParcel;
import android.healthconnect.aidl.ChangeLogTokenRequestParcel;
import android.healthconnect.aidl.ChangeLogsResponseParcel;
import android.healthconnect.aidl.DeleteUsingFiltersRequestParcel;
import android.healthconnect.aidl.HealthConnectExceptionParcel;
import android.healthconnect.aidl.IAggregateRecordsResponseCallback;
import android.healthconnect.aidl.IApplicationInfoResponseCallback;
import android.healthconnect.aidl.IChangeLogsResponseCallback;
import android.healthconnect.aidl.IEmptyResponseCallback;
import android.healthconnect.aidl.IGetChangeLogTokenCallback;
import android.healthconnect.aidl.IHealthConnectService;
import android.healthconnect.aidl.IInsertRecordsResponseCallback;
import android.healthconnect.aidl.IReadRecordsResponseCallback;
import android.healthconnect.aidl.IRecordTypeInfoResponseCallback;
import android.healthconnect.aidl.InsertRecordsResponseParcel;
import android.healthconnect.aidl.ReadRecordsRequestParcel;
import android.healthconnect.aidl.RecordIdFiltersParcel;
import android.healthconnect.aidl.RecordTypeInfoResponseParcel;
import android.healthconnect.aidl.RecordsParcel;
import android.healthconnect.datatypes.AggregationType;
import android.healthconnect.datatypes.DataOrigin;
import android.healthconnect.datatypes.Record;
import android.healthconnect.internal.datatypes.RecordInternal;
import android.healthconnect.internal.datatypes.utils.InternalExternalRecordConverter;
import android.os.Binder;
import android.os.OutcomeReceiver;
import android.os.RemoteException;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
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;
/**
* 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.healthconnect.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.
*
* @hide
*/
@SystemApi
@SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_MANAGE_HEALTH_PERMISSIONS =
"android.healthconnect.action.MANAGE_HEALTH_PERMISSIONS";
/**
* Activity action: Launch UI to show and manage (e.g. grant/revoke) health permissions and
* health data (e.g. delete) for an app.
*
* <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.
*
* @hide
*/
@SystemApi
@SdkConstant(SdkConstant.SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_MANAGE_HEALTH_PERMISSIONS_AND_DATA =
"android.healthconnect.action.MANAGE_HEALTH_PERMISSIONS_AND_DATA";
/**
* 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.healthconnect.action.HEALTH_HOME_SETTINGS";
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();
}
/**
* 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 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 = null;
try {
final PackageManager pm = context.getApplicationContext().getPackageManager();
final PermissionGroupInfo permGroupInfo =
pm.getPermissionGroupInfo(
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 (HealthPermissions.HEALTH_PERMISSION_GROUP.equals(perm.group)) {
permissions.add(perm.name);
}
}
sHealthPermissions = Collections.unmodifiableSet(permissions);
return sHealthPermissions;
}
/**
* 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(HealthPermissions.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(HealthPermissions.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(HealthPermissions.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(HealthPermissions.MANAGE_HEALTH_PERMISSIONS)
@UserHandleAware
public List<String> getGrantedHealthPermissions(@NonNull String packageName) {
try {
return mService.getGrantedHealthPermissions(packageName, mContext.getUser());
} 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.
* <p>TODO(b/251194265): User permission checks once available.
* @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 {
List<RecordInternal<?>> recordInternals =
mInternalExternalRecordConverter.getInternalRecords(records);
mService.insertRecords(
mContext.getPackageName(),
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
* <p>TODO(b/251194265): User permission checks once available.
*/
@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.getPackageName(),
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) {
callback.onError(
new HealthConnectException(
HealthConnectException.ERROR_INTERNAL,
classCastException.getMessage()));
} 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.
* <p>TODO(b/251194265): User permission checks once available.
* @hide
*/
@SystemApi
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.getPackageName(),
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.
* <p>TODO(b/251194265): User permission checks once available.
* @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.deleteUsingFilters(
mContext.getPackageName(),
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.
* <p>TODO(b/251194265): User permission checks once available.
*/
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.deleteUsingFilters(
mContext.getPackageName(),
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 token 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.
* <p>TODO(b/251194265): User permission checks once available.
* @hide
* @see HealthConnectManager#getChangeLogToken
*/
public void getChangeLogs(
long token,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<ChangeLogsResponse, HealthConnectException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
try {
mService.getChangeLogs(
mContext.getPackageName(),
token,
new IChangeLogsResponseCallback.Stub() {
@Override
public void onResult(ChangeLogsResponseParcel parcel) {
Binder.clearCallingIdentity();
executor.execute(
() -> {
try {
callback.onResult(parcel.getChangeLogsResponse());
} catch (InvocationTargetException
| InstantiationException
| IllegalAccessException
| NoSuchMethodException e) {
callback.onError(
new HealthConnectException(
HealthConnectException.ERROR_INTERNAL));
}
});
}
@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.
* <p>TODO(b/251194265): User permission checks once available.
*/
public void getChangeLogToken(
@NonNull ChangeLogTokenRequest request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<Long, HealthConnectException> callback) {
try {
mService.getChangeLogToken(
mContext.getPackageName(),
new ChangeLogTokenRequestParcel(request),
new IGetChangeLogTokenCallback.Stub() {
@Override
public void onResult(long token) {
Binder.clearCallingIdentity();
executor.execute(() -> callback.onResult(token));
}
@Override
public void onError(HealthConnectExceptionParcel exception) {
returnError(executor, exception, callback);
}
});
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* @hide
* <p>Retrieves {@link android.healthconnect.RecordTypeInfoResponse} for each RecordType.
* @param executor Executor on which to invoke the callback.
* @param callback Callback to receive result of performing this operation.
*/
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();
}
}
/**
* API to read records based on {@link RecordIdFilter}.
*
* @param request ReadRecordsRequestUsingIds request containing a list of {@link RecordIdFilter}
* and recordType to perform read operation.
* @param executor Executor on which to invoke the callback.
* @param callback Callback to receive result of performing this operation.
* <p>TODO(b/251194265): User permission checks once available.
*/
public <T extends Record> void readRecords(
@NonNull ReadRecordsRequestUsingIds<T> request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
try {
mService.readRecords(
mContext.getPackageName(),
new ReadRecordsRequestParcel(request),
getReadCallback(executor, callback));
} catch (RemoteException remoteException) {
remoteException.rethrowFromSystemServer();
}
}
/**
* API to read records based on {@link ReadRecordsRequestUsingFilters}.
*
* @param request Read request based on {@link ReadRecordsRequestUsingFilters}
* @param executor Executor on which to invoke the callback.
* @param callback Callback to receive result of performing this operation.
* <p>TODO(b/251194265): User permission checks once available.
*/
public <T extends Record> void readRecords(
@NonNull ReadRecordsRequestUsingFilters<T> request,
@NonNull Executor executor,
@NonNull OutcomeReceiver<ReadRecordsResponse<T>, HealthConnectException> callback) {
Objects.requireNonNull(request);
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
try {
mService.readRecords(
mContext.getPackageName(),
new ReadRecordsRequestParcel(request),
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.
*/
// TODO(b/251194265): User permission checks once available.
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 {
// verify that the package requesting the change is same as the packageName in the
// records.
String contextPackageName = mContext.getPackageName();
for (Record record : records) {
if (!Objects.equals(
contextPackageName,
record.getMetadata().getDataOrigin().getPackageName())) {
throw new IllegalArgumentException(
"The package requesting the change does not match "
+ "the packageName in input records");
}
}
List<RecordInternal<?>> recordInternals =
mInternalExternalRecordConverter.getInternalRecords(records);
// Verify if the input record has clientRecordId or UUID.
for (RecordInternal<?> recordInternal : recordInternals) {
if ((recordInternal.getClientRecordId() == null
|| recordInternal.getClientRecordId().isEmpty())
&& (recordInternal.getUuid() == null
|| recordInternal.getUuid().isEmpty())) {
throw new IllegalArgumentException(
"At least one of the records is missing both ClientRecordID"
+ " and UUID. RecordType of the input: "
+ recordInternal.getRecordType());
}
}
mService.updateRecords(
mContext.getPackageName(),
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.getMessage());
} 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.
* <p>TODO(b/251194265): User permission checks once available.
* <p>TODO(b/260069905): Add privileges permission for HC UI only.
* @hide
*/
@NonNull
@SystemApi
@RequiresPermission(QUERY_ALL_PACKAGES)
public void getContributorApplicationsInfo(
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<ApplicationInfoResponse, HealthConnectException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
if (mContext.checkCallingOrSelfPermission(QUERY_ALL_PACKAGES) != PERMISSION_GRANTED) {
throw new SecurityException(
"Caller does not hold " + android.Manifest.permission.QUERY_ALL_PACKAGES);
}
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();
}
}
@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(RecordsParcel parcel) {
Binder.clearCallingIdentity();
try {
List<T> externalRecords =
(List<T>)
mInternalExternalRecordConverter.getExternalRecords(
parcel.getRecords());
executor.execute(
() -> callback.onResult(new ReadRecordsResponse<>(externalRecords)));
} 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()));
}
}