blob: 9ed0a60b3d24930ad0d68bc85756c924292023c9 [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 static java.util.Collections.emptySet;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PermissionInfo;
import android.database.sqlite.SQLiteException;
import android.healthconnect.HealthConnectException;
import android.healthconnect.HealthPermissions;
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.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.util.Slog;
import com.android.server.healthconnect.storage.TransactionManager;
import com.android.server.healthconnect.storage.request.InsertTransactionRequest;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
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 InsertTransactionRequest(
packageName, recordsParcel.getRecords(), mContext));
callback.onResult(new InsertRecordsResponseParcel(uuids));
} 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);
}
});
}
/**
* Returns a 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.
*/
static Set<String> getDefinedHealthPerms(PackageManager packageManager) {
PermissionInfo[] permissionInfos =
getHealthPermissionControllerPermissionInfos(packageManager);
if (permissionInfos == null) {
// This should never happen. But if it does, let's mark our permissions infra as
// inactive. At least users can use other parts of their phone.
return emptySet();
}
Set<String> definedHealthPerms = new HashSet<>(permissionInfos.length);
for (PermissionInfo permInfo : permissionInfos) {
if (HealthPermissions.HEALTH_PERMISSION_GROUP.equals(permInfo.group)) {
definedHealthPerms.add(permInfo.name);
}
}
if (DEBUG) {
Slog.d(TAG, "Defined health permissions: " + definedHealthPerms.toString());
}
return definedHealthPerms;
}
/**
* Returns a list of permissions defined in the health permission controller APK, {@code null}
* if it could not be retrieved.
*/
private static PermissionInfo[] getHealthPermissionControllerPermissionInfos(
PackageManager packageManager) {
PackageInfo packageInfo;
String healthConnectControllerPackageName = null;
try {
healthConnectControllerPackageName =
packageManager.getPermissionInfo(
HealthPermissions.MANAGE_HEALTH_PERMISSIONS, /* flags= */ 0)
.packageName;
packageInfo =
packageManager.getPackageInfo(
healthConnectControllerPackageName,
PackageManager.PackageInfoFlags.of(PackageManager.GET_PERMISSIONS));
} catch (PackageManager.NameNotFoundException e) {
// This should never happen. But if it does, let's log it and return null
if (healthConnectControllerPackageName == null) {
// We couldn't find the permission
Slog.e(
TAG,
"HealthConnect permission"
+ HealthPermissions.MANAGE_HEALTH_PERMISSIONS
+ ") not found");
} else {
// we couldn't find the package
Slog.e(
TAG,
"HealthConnect permissions APK ("
+ healthConnectControllerPackageName
+ ") not found");
}
return null;
}
if (packageInfo.permissions == null) {
// This should never happen. But if it does, let's log it and return null.
Slog.e(
TAG,
"No HealthConnect permissions defined in APK ("
+ healthConnectControllerPackageName
+ ")");
return null;
}
return packageInfo.permissions;
}
private static void tryAndThrowException(
@NonNull IInsertRecordsResponseCallback 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);
}
}
}