blob: b77e4e5b2f100b9dde91ae924e2f4989d24c4e43 [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 com.android.server.healthconnect;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.healthconnect.AggregateRecordsResponse;
import android.healthconnect.HealthConnectException;
import android.healthconnect.aidl.AggregateDataRequestParcel;
import android.healthconnect.aidl.AggregateDataResponseParcel;
import android.healthconnect.HealthConnectManager;
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.AppInfo;
import android.healthconnect.datatypes.DataOrigin;
import android.healthconnect.datatypes.Record;
import android.healthconnect.internal.datatypes.RecordInternal;
import android.healthconnect.internal.datatypes.utils.RecordMapper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Slog;
import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
import com.android.server.healthconnect.storage.TransactionManager;
import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsHelper;
import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsRequestHelper;
import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
import com.android.server.healthconnect.storage.request.DeleteTransactionRequest;
import com.android.server.healthconnect.storage.request.AggregateTransactionRequest;
import com.android.server.healthconnect.storage.request.ReadTransactionRequest;
import com.android.server.healthconnect.storage.request.UpsertTransactionRequest;
import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* IHealthConnectService's implementation
*
* @hide
*/
final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
private static final String TAG = "HealthConnectService";
private static final int MIN_BACKGROUND_EXECUTOR_THREADS = 2;
private static final long KEEP_ALIVE_TIME = 60L;
private static final boolean DEBUG = false;
// In order to unblock the binder queue all the async should be scheduled on SHARED_EXECUTOR, as
// soon as they come.
private static final Executor SHARED_EXECUTOR =
new ThreadPoolExecutor(
Math.max(
MIN_BACKGROUND_EXECUTOR_THREADS,
Runtime.getRuntime().availableProcessors()),
Math.max(
MIN_BACKGROUND_EXECUTOR_THREADS,
Runtime.getRuntime().availableProcessors()),
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
private final TransactionManager mTransactionManager;
private final HealthConnectPermissionHelper mPermissionHelper;
private final Context mContext;
HealthConnectServiceImpl(
TransactionManager transactionManager,
HealthConnectPermissionHelper permissionHelper,
Context context) {
mTransactionManager = transactionManager;
mPermissionHelper = permissionHelper;
mContext = context;
}
@Override
public void grantHealthPermission(
@NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) {
mPermissionHelper.grantHealthPermission(packageName, permissionName, user);
}
@Override
public void revokeHealthPermission(
@NonNull String packageName,
@NonNull String permissionName,
@Nullable String reason,
@NonNull UserHandle user) {
mPermissionHelper.revokeHealthPermission(packageName, permissionName, reason, user);
}
@Override
public void revokeAllHealthPermissions(
@NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) {
mPermissionHelper.revokeAllHealthPermissions(packageName, reason, user);
}
@Override
public List<String> getGrantedHealthPermissions(
@NonNull String packageName, @NonNull UserHandle user) {
return mPermissionHelper.getGrantedHealthPermissions(packageName, user);
}
/**
* Inserts {@code recordsParcel} into the HealthConnect database.
*
* @param recordsParcel parcel for list of records to be inserted.
* @param callback Callback to receive result of performing this operation. The keys returned in
* {@link InsertRecordsResponseParcel} are the unique IDs of the input records. The values
* are in same order as {@code record}. In case of an error or a permission failure the
* HealthConnect service, {@link IInsertRecordsResponseCallback#onError} will be invoked
* with a {@link HealthConnectExceptionParcel}.
*/
@Override
public void insertRecords(
@NonNull String packageName,
@NonNull RecordsParcel recordsParcel,
@NonNull IInsertRecordsResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
List<String> uuids =
mTransactionManager.insertAll(
new UpsertTransactionRequest(
packageName,
recordsParcel.getRecords(),
mContext,
/* isInsertRequest */ true));
callback.onResult(new InsertRecordsResponseParcel(uuids));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* Returns aggregation results based on the {@code request} into the HealthConnect database.
*
* @param packageName name of the package inserting the record.
* @param request represents the request using which the aggregation is to be performed.
* @param callback Callback to receive result of performing this operation.
*/
public void aggregateRecords(
String packageName,
AggregateDataRequestParcel request,
IAggregateRecordsResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
Map<Integer, AggregateRecordsResponse.AggregateResult> results =
mTransactionManager.getAggregations(
new AggregateTransactionRequest(packageName, request));
callback.onResult(
new AggregateDataResponseParcel(
new AggregateRecordsResponse(results)));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception e) {
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* Read records {@code recordsParcel} from HealthConnect database.
*
* @param packageName packageName of calling app.
* @param request ReadRecordsRequestParcel is parcel for the request object containing {@link
* RecordIdFiltersParcel}.
* @param callback Callback to receive result of performing this operation. The records are
* returned in {@link RecordsParcel} . In case of an error or a permission failure the
* HealthConnect service, {@link IReadRecordsResponseCallback#onError} will be invoked with
* a {@link HealthConnectExceptionParcel}.
*/
@Override
public void readRecords(
@NonNull String packageName,
@NonNull ReadRecordsRequestParcel request,
@NonNull IReadRecordsResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
try {
List<RecordInternal<?>> recordInternalList =
mTransactionManager.readRecords(
new ReadTransactionRequest(packageName, request));
callback.onResult(new RecordsParcel(recordInternalList));
} catch (TypeNotPresentException exception) {
// All the requested package names are not present, so simply return
// an empty list
if (ReadTransactionRequest.TYPE_NOT_PRESENT_PACKAGE_NAME.equals(
exception.typeName())) {
callback.onResult(new RecordsParcel(new ArrayList<>()));
} else {
throw exception;
}
}
} catch (SQLiteException sqLiteException) {
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception e) {
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* @see HealthConnectManager#getChangeLogToken
*/
@Override
public void getChangeLogToken(
@NonNull String packageName,
@NonNull ChangeLogTokenRequestParcel request,
@NonNull IGetChangeLogTokenCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
callback.onResult(
ChangeLogsRequestHelper.getInstance()
.getToken(packageName, request));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception e) {
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* Updates {@code recordsParcel} into the HealthConnect database.
*
* @param recordsParcel parcel for list of records to be updated.
* @param callback Callback to receive result of performing this operation. In case of an error
* or a permission failure the HealthConnect service, {@link IEmptyResponseCallback#onError}
* will be invoked with a {@link HealthConnectException}.
*/
@Override
public void updateRecords(
@NonNull String packageName,
@NonNull RecordsParcel recordsParcel,
@NonNull IEmptyResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
mTransactionManager.updateAll(
new UpsertTransactionRequest(
packageName,
recordsParcel.getRecords(),
mContext,
/* isInsertRequest */ false));
callback.onResult();
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SqlException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (IllegalArgumentException illegalArgumentException) {
Slog.e(TAG, "Exception: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_INVALID_ARGUMENT);
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* @hide
* @see HealthConnectManager#getChangeLogs
*/
@Override
public void getChangeLogs(
@NonNull String packageName,
@NonNull long token,
IChangeLogsResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
ChangeLogsRequestHelper.TokenRequest changeLogsTokenRequest =
ChangeLogsRequestHelper.getRequest(token);
final Map<Integer, ChangeLogsHelper.ChangeLogs> operationTypeToUUIds =
ChangeLogsHelper.getInstance()
.getChangeLogs(changeLogsTokenRequest);
List<RecordInternal<?>> recordInternals =
mTransactionManager.readRecords(
new ReadTransactionRequest(
ChangeLogsHelper.getRecordTypeToInsertedUuids(
operationTypeToUUIds)));
callback.onResult(
new ChangeLogsResponseParcel(
new RecordsParcel(recordInternals),
ChangeLogsHelper.getDeletedIds(operationTypeToUUIds)));
} catch (IllegalArgumentException illegalArgumentException) {
Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_INVALID_ARGUMENT);
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(
callback, exception, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* API to delete records based on {@code request}
*
* <p>NOTE: Internally we only need a single API to handle deletes as SDK code transform all its
* delete requests to {@link DeleteUsingFiltersRequestParcel}
*/
@Override
public void deleteUsingFilters(
@NonNull String packageName,
@NonNull DeleteUsingFiltersRequestParcel request,
@NonNull IEmptyResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
mTransactionManager.deleteAll(
new DeleteTransactionRequest(packageName, request));
callback.onResult();
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (IllegalArgumentException illegalArgumentException) {
Slog.e(TAG, "SQLiteException: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_SECURITY);
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(
callback, exception, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* Returns information, represented by {@code ApplicationInfoResponse}, for all the packages
* that have contributed to the health connect DB.
*
* @param callback Callback to receive result of performing this operation. In case of an error
* or a permission failure the HealthConnect service, {@link IEmptyResponseCallback#onError}
* will be invoked with a {@link HealthConnectException}.
*/
@Override
public void getContributorApplicationsInfo(@NonNull IApplicationInfoResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
List<AppInfo> applicationInfos =
AppInfoHelper.getInstance().getApplicationInfos();
callback.onResult(new ApplicationInfoResponseParcel(applicationInfos));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SqlException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
/** Retrieves {@link android.healthconnect.RecordTypeInfoResponse} for each RecordType. */
@Override
public void queryAllRecordTypesInfo(@NonNull IRecordTypeInfoResponseCallback callback) {
SHARED_EXECUTOR.execute(
() -> {
try {
try {
callback.onResult(
new RecordTypeInfoResponseParcel(
getPopulatedRecordTypeInfoResponses()));
} catch (SQLiteException sqLiteException) {
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
}
} catch (Exception exception) {
tryAndThrowException(
callback, exception, HealthConnectException.ERROR_INTERNAL);
}
});
}
private Map<Integer, List<DataOrigin>> getPopulatedRecordTypeInfoResponses() {
Map<Integer, Class<? extends Record>> recordIdToExternalRecordClassMap =
RecordMapper.getInstance().getRecordIdToExternalRecordClassMap();
Map<Integer, List<DataOrigin>> recordTypeInfoResponses =
new ArrayMap<>(recordIdToExternalRecordClassMap.size());
recordIdToExternalRecordClassMap
.keySet()
.forEach(
(recordType) -> {
RecordHelper<?> recordHelper =
RecordHelperProvider.getInstance().getRecordHelper(recordType);
Objects.requireNonNull(recordHelper);
List<DataOrigin> packages =
mTransactionManager.getDistinctPackageNamesForRecordTable(
recordHelper);
recordTypeInfoResponses.put(recordType, packages);
});
return recordTypeInfoResponses;
}
private static void tryAndThrowException(
@NonNull IInsertRecordsResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.getMessage())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IAggregateRecordsResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.getMessage())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IReadRecordsResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.getMessage())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IGetChangeLogTokenCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.getMessage())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IEmptyResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.toString())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IApplicationInfoResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.getMessage())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IChangeLogsResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.toString())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IRecordTypeInfoResponseCallback callback,
@NonNull Exception exception,
@HealthConnectException.ErrorCode int errorCode) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, exception.getMessage())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
}