| /* |
| * 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; |
| } |
| } |