blob: bc89e57fcc611b1bbaca5e3c0efa2c5b998ac056 [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 android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemService;
import android.annotation.UserHandleAware;
import android.content.Context;
import android.healthconnect.aidl.HealthConnectExceptionParcel;
import android.healthconnect.aidl.IHealthConnectService;
import android.healthconnect.aidl.IInsertRecordsResponseCallback;
import android.healthconnect.aidl.InsertRecordsResponseParcel;
import android.healthconnect.aidl.RecordsParcel;
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 java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.Objects;
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 {
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(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.
*/
public void insertRecords(
@NonNull List<Record> records,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<InsertRecordsResponse, HealthConnectException> callback) {
Objects.requireNonNull(records);
/*
TODO(b/251454017): Use executor to return results after "Hidden API flags are
inconsistent" error is fixed
*/
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();
callback.onResult(
new InsertRecordsResponse(
getRecordsWithUids(records, parcel.getUids())));
}
@Override
public void onError(HealthConnectExceptionParcel exception) {
Binder.clearCallingIdentity();
callback.onError(exception.getHealthConnectException());
}
});
} catch (ArithmeticException | ClassCastException invalidArgumentException) {
callback.onError(
new HealthConnectException(
HealthConnectException.ERROR_INVALID_ARGUMENT,
invalidArgumentException.getMessage()));
} catch (IllegalAccessException
| InstantiationException
| InvocationTargetException
| NoSuchMethodException exception) {
callback.onError(
new HealthConnectException(
HealthConnectException.ERROR_INTERNAL, exception.getMessage()));
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
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;
}
}