blob: 578db339707d46656db4ce820f8a8defd5eec5ed [file] [log] [blame]
/*
* Copyright (C) 2023 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 android.Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.health.connect.Constants.DEFAULT_LONG;
import static android.health.connect.Constants.READ;
import static android.health.connect.HealthConnectException.ERROR_INTERNAL;
import static android.health.connect.HealthConnectException.ERROR_SECURITY;
import static android.health.connect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.DELETE_DATA;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.GET_CHANGES;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.GET_CHANGES_TOKEN;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.INSERT_DATA;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.READ_AGGREGATED_DATA;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.READ_DATA;
import static com.android.server.healthconnect.logging.HealthConnectServiceLogger.ApiMethods.UPDATE_DATA;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.AttributionSource;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.sqlite.SQLiteException;
import android.health.connect.Constants;
import android.health.connect.FetchDataOriginsPriorityOrderResponse;
import android.health.connect.HealthConnectDataState;
import android.health.connect.HealthConnectException;
import android.health.connect.HealthConnectManager;
import android.health.connect.HealthConnectManager.DataDownloadState;
import android.health.connect.HealthDataCategory;
import android.health.connect.HealthPermissions;
import android.health.connect.RecordTypeInfoResponse;
import android.health.connect.accesslog.AccessLog;
import android.health.connect.accesslog.AccessLogsResponseParcel;
import android.health.connect.aidl.ActivityDatesRequestParcel;
import android.health.connect.aidl.ActivityDatesResponseParcel;
import android.health.connect.aidl.AggregateDataRequestParcel;
import android.health.connect.aidl.ApplicationInfoResponseParcel;
import android.health.connect.aidl.DeleteUsingFiltersRequestParcel;
import android.health.connect.aidl.GetPriorityResponseParcel;
import android.health.connect.aidl.HealthConnectExceptionParcel;
import android.health.connect.aidl.IAccessLogsResponseCallback;
import android.health.connect.aidl.IActivityDatesResponseCallback;
import android.health.connect.aidl.IAggregateRecordsResponseCallback;
import android.health.connect.aidl.IApplicationInfoResponseCallback;
import android.health.connect.aidl.IChangeLogsResponseCallback;
import android.health.connect.aidl.IDataStagingFinishedCallback;
import android.health.connect.aidl.IEmptyResponseCallback;
import android.health.connect.aidl.IGetChangeLogTokenCallback;
import android.health.connect.aidl.IGetHealthConnectDataStateCallback;
import android.health.connect.aidl.IGetHealthConnectMigrationUiStateCallback;
import android.health.connect.aidl.IGetPriorityResponseCallback;
import android.health.connect.aidl.IHealthConnectService;
import android.health.connect.aidl.IInsertRecordsResponseCallback;
import android.health.connect.aidl.IMigrationCallback;
import android.health.connect.aidl.IReadRecordsResponseCallback;
import android.health.connect.aidl.IRecordTypeInfoResponseCallback;
import android.health.connect.aidl.InsertRecordsResponseParcel;
import android.health.connect.aidl.ReadRecordsRequestParcel;
import android.health.connect.aidl.ReadRecordsResponseParcel;
import android.health.connect.aidl.RecordIdFiltersParcel;
import android.health.connect.aidl.RecordTypeInfoResponseParcel;
import android.health.connect.aidl.RecordsParcel;
import android.health.connect.aidl.UpdatePriorityRequestParcel;
import android.health.connect.changelog.ChangeLogTokenRequest;
import android.health.connect.changelog.ChangeLogTokenResponse;
import android.health.connect.changelog.ChangeLogsRequest;
import android.health.connect.changelog.ChangeLogsResponse;
import android.health.connect.changelog.ChangeLogsResponse.DeletedLog;
import android.health.connect.datatypes.AppInfo;
import android.health.connect.datatypes.DataOrigin;
import android.health.connect.datatypes.Record;
import android.health.connect.internal.datatypes.RecordInternal;
import android.health.connect.internal.datatypes.utils.AggregationTypeIdMapper;
import android.health.connect.internal.datatypes.utils.RecordMapper;
import android.health.connect.internal.datatypes.utils.RecordTypePermissionCategoryMapper;
import android.health.connect.migration.HealthConnectMigrationUiState;
import android.health.connect.migration.MigrationEntityParcel;
import android.health.connect.migration.MigrationException;
import android.health.connect.ratelimiter.RateLimiter;
import android.health.connect.ratelimiter.RateLimiter.QuotaCategory;
import android.health.connect.ratelimiter.RateLimiterException;
import android.health.connect.restore.BackupFileNamesSet;
import android.health.connect.restore.StageRemoteDataException;
import android.health.connect.restore.StageRemoteDataRequest;
import android.os.Binder;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.os.RemoteException;
import android.os.Trace;
import android.os.UserHandle;
import android.permission.PermissionManager;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalManagerRegistry;
import com.android.server.appop.AppOpsManagerLocal;
import com.android.server.healthconnect.backuprestore.BackupRestore;
import com.android.server.healthconnect.logging.HealthConnectServiceLogger;
import com.android.server.healthconnect.migration.DataMigrationManager;
import com.android.server.healthconnect.migration.MigrationCleaner;
import com.android.server.healthconnect.migration.MigrationStateManager;
import com.android.server.healthconnect.migration.MigrationUiStateManager;
import com.android.server.healthconnect.migration.PriorityMigrationHelper;
import com.android.server.healthconnect.permission.DataPermissionEnforcer;
import com.android.server.healthconnect.permission.FirstGrantTimeManager;
import com.android.server.healthconnect.permission.HealthConnectPermissionHelper;
import com.android.server.healthconnect.storage.AutoDeleteService;
import com.android.server.healthconnect.storage.TransactionManager;
import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper;
import com.android.server.healthconnect.storage.datatypehelpers.ActivityDateHelper;
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.DeviceInfoHelper;
import com.android.server.healthconnect.storage.datatypehelpers.HealthDataCategoryPriorityHelper;
import com.android.server.healthconnect.storage.datatypehelpers.MigrationEntityHelper;
import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
import com.android.server.healthconnect.storage.request.AggregateTransactionRequest;
import com.android.server.healthconnect.storage.request.DeleteTransactionRequest;
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.io.IOException;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
/**
* IHealthConnectService's implementation
*
* @hide
*/
final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
private static final String TAG = "HealthConnectService";
// Permission for test api for deleting staged data
private static final String DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA_PERMISSION =
"android.permission.DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA";
// Allows an application to act as a backup inter-agent to send and receive HealthConnect data
private static final String HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION =
"android.permission.HEALTH_CONNECT_BACKUP_INTER_AGENT";
private static final String TAG_INSERT = "HealthConnectInsert";
private static final String TAG_READ = "HealthConnectRead";
private static final String TAG_GRANT_PERMISSION = "HealthConnectGrantReadPermissions";
private static final String TAG_READ_PERMISSION = "HealthConnectReadPermission";
private static final String TAG_INSERT_SUBTASKS = "HealthConnectInsertSubtasks";
private static final String TAG_DELETE_SUBTASKS = "HealthConnectDeleteSubtasks";
private static final String TAG_READ_SUBTASKS = "HealthConnectReadSubtasks";
private static final int TRACE_TAG_INSERT = TAG_INSERT.hashCode();
private static final int TRACE_TAG_READ = TAG_READ.hashCode();
private static final int TRACE_TAG_GRANT_PERMISSION = TAG_GRANT_PERMISSION.hashCode();
private static final int TRACE_TAG_READ_PERMISSION = TAG_READ_PERMISSION.hashCode();
private static final int TRACE_TAG_INSERT_SUBTASKS = TAG_INSERT_SUBTASKS.hashCode();
private static final int TRACE_TAG_DELETE_SUBTASKS = TAG_DELETE_SUBTASKS.hashCode();
private static final int TRACE_TAG_READ_SUBTASKS = TAG_READ_SUBTASKS.hashCode();
private final TransactionManager mTransactionManager;
private final HealthConnectPermissionHelper mPermissionHelper;
private final FirstGrantTimeManager mFirstGrantTimeManager;
private final Context mContext;
private final PermissionManager mPermissionManager;
private final BackupRestore mBackupRestore;
private final MigrationStateManager mMigrationStateManager;
private final DataPermissionEnforcer mDataPermissionEnforcer;
private final AppOpsManagerLocal mAppOpsManagerLocal;
private final MigrationUiStateManager mMigrationUiStateManager;
private volatile UserHandle mCurrentForegroundUser;
HealthConnectServiceImpl(
TransactionManager transactionManager,
HealthConnectPermissionHelper permissionHelper,
MigrationCleaner migrationCleaner,
FirstGrantTimeManager firstGrantTimeManager,
MigrationStateManager migrationStateManager,
MigrationUiStateManager migrationUiStateManager,
Context context) {
mTransactionManager = transactionManager;
mPermissionHelper = permissionHelper;
mFirstGrantTimeManager = firstGrantTimeManager;
mContext = context;
mCurrentForegroundUser = context.getUser();
mPermissionManager = mContext.getSystemService(PermissionManager.class);
mMigrationStateManager = migrationStateManager;
mDataPermissionEnforcer = new DataPermissionEnforcer(mPermissionManager, mContext);
mAppOpsManagerLocal = LocalManagerRegistry.getManager(AppOpsManagerLocal.class);
mBackupRestore =
new BackupRestore(mFirstGrantTimeManager, mMigrationStateManager, mContext);
mMigrationUiStateManager = migrationUiStateManager;
migrationCleaner.attachTo(migrationStateManager);
mMigrationUiStateManager.attachTo(migrationStateManager);
}
public void onUserSwitching(UserHandle currentForegroundUser) {
mCurrentForegroundUser = currentForegroundUser;
mBackupRestore.setupForUser(currentForegroundUser);
}
@Override
public void grantHealthPermission(
@NonNull String packageName, @NonNull String permissionName, @NonNull UserHandle user) {
throwIllegalStateExceptionIfDataSyncInProgress();
Trace.traceBegin(TRACE_TAG_GRANT_PERMISSION, TAG_GRANT_PERMISSION);
mPermissionHelper.grantHealthPermission(packageName, permissionName, user);
Trace.traceEnd(TRACE_TAG_GRANT_PERMISSION);
}
@Override
public void revokeHealthPermission(
@NonNull String packageName,
@NonNull String permissionName,
@Nullable String reason,
@NonNull UserHandle user) {
throwIllegalStateExceptionIfDataSyncInProgress();
mPermissionHelper.revokeHealthPermission(packageName, permissionName, reason, user);
}
@Override
public void revokeAllHealthPermissions(
@NonNull String packageName, @Nullable String reason, @NonNull UserHandle user) {
throwIllegalStateExceptionIfDataSyncInProgress();
mPermissionHelper.revokeAllHealthPermissions(packageName, reason, user);
}
@Override
public List<String> getGrantedHealthPermissions(
@NonNull String packageName, @NonNull UserHandle user) {
throwIllegalStateExceptionIfDataSyncInProgress();
Trace.traceBegin(TRACE_TAG_READ_PERMISSION, TAG_READ_PERMISSION);
List<String> grantedPermissions =
mPermissionHelper.getGrantedHealthPermissions(packageName, user);
Trace.traceEnd(TRACE_TAG_READ_PERMISSION);
return grantedPermissions;
}
@Override
public long getHistoricalAccessStartDateInMilliseconds(
@NonNull String packageName, @NonNull UserHandle userHandle) {
throwIllegalStateExceptionIfDataSyncInProgress();
Instant date = mPermissionHelper.getHealthDataStartDateAccess(packageName, userHandle);
if (date == null) {
return Constants.DEFAULT_LONG;
} else {
return date.toEpochMilli();
}
}
/**
* 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 AttributionSource attributionSource,
@NonNull RecordsParcel recordsParcel,
@NonNull IInsertRecordsResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(false, INSERT_DATA)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
if (hasDataManagementPermission(uid, pid)) {
throw new SecurityException(
"Apps with android.permission.MANAGE_HEALTH_DATA permission are"
+ " not allowed to insert records");
}
enforceMemoryRateLimit(
recordsParcel.getRecordsSize(),
recordsParcel.getRecordsChunkSize());
final List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
builder.setNumberOfRecords(recordInternals.size());
throwExceptionIfDataSyncInProgress();
mDataPermissionEnforcer.enforceRecordsWritePermissions(
recordInternals, attributionSource);
boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
tryAcquireApiCallQuota(
uid, QuotaCategory.QUOTA_CATEGORY_WRITE, isInForeground, builder);
Trace.traceBegin(TRACE_TAG_INSERT, TAG_INSERT);
UpsertTransactionRequest insertRequest =
new UpsertTransactionRequest(
attributionSource.getPackageName(),
recordInternals,
mContext,
/* isInsertRequest */ true,
mDataPermissionEnforcer
.collectExtraWritePermissionStateMapping(
recordInternals, attributionSource));
List<String> uuids = mTransactionManager.insertAll(insertRequest);
tryAndReturnResult(callback, uuids, builder);
HealthConnectThreadScheduler.scheduleInternalTask(
() -> postInsertTasks(attributionSource, recordsParcel));
finishDataDeliveryWriteRecords(recordInternals, attributionSource);
logRecordTypeSpecificUpsertMetrics(
recordInternals, attributionSource.getPackageName());
builder.setDataTypesFromRecordInternals(recordInternals);
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, ERROR_INTERNAL);
} finally {
Trace.traceEnd(TRACE_TAG_INSERT);
builder.build().log();
}
},
uid,
false);
}
private void postInsertTasks(
@NonNull AttributionSource attributionSource, @NonNull RecordsParcel recordsParcel) {
Trace.traceBegin(TRACE_TAG_INSERT_SUBTASKS, TAG_INSERT.concat("PostInsertTasks"));
ActivityDateHelper.getInstance().insertRecordDate(recordsParcel.getRecords());
Set<Integer> recordsTypesInsertedSet =
recordsParcel.getRecords().stream()
.map(RecordInternal::getRecordType)
.collect(Collectors.toSet());
// Update AppInfo table with the record types of records inserted in the request for the
// current package.
AppInfoHelper.getInstance()
.updateAppInfoRecordTypesUsedOnInsert(
recordsTypesInsertedSet, attributionSource.getPackageName());
Trace.traceEnd(TRACE_TAG_INSERT_SUBTASKS);
}
/**
* Returns aggregation results based on the {@code request} into the HealthConnect database.
*
* @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(
@NonNull AttributionSource attributionSource,
AggregateDataRequestParcel request,
IAggregateRecordsResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(
holdsDataManagementPermission, READ_AGGREGATED_DATA)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
builder.setNumberOfRecords(request.getAggregateIds().length);
throwExceptionIfDataSyncInProgress();
List<Integer> recordTypesToTest = new ArrayList<>();
for (int aggregateId : request.getAggregateIds()) {
recordTypesToTest.addAll(
AggregationTypeIdMapper.getInstance()
.getAggregationTypeFor(aggregateId)
.getApplicableRecordTypeIds());
}
if (!holdsDataManagementPermission) {
boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
if (!isInForeground) {
throwSecurityException(
attributionSource.getPackageName()
+ "must be in foreground to call aggregate method");
}
mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
recordTypesToTest, attributionSource);
tryAcquireApiCallQuota(
uid,
RateLimiter.QuotaCategory.QUOTA_CATEGORY_READ,
isInForeground,
builder);
}
callback.onResult(
new AggregateTransactionRequest(
attributionSource.getPackageName(), request)
.getAggregateDataResponseParcel());
finishDataDeliveryRead(recordTypesToTest, attributionSource);
builder.setDataTypesFromRecordTypes(recordTypesToTest)
.setHealthDataServiceApiStatusSuccess();
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, ERROR_INTERNAL);
} finally {
builder.build().log();
}
},
uid,
holdsDataManagementPermission);
}
/**
* Read records {@code recordsParcel} from HealthConnect database.
*
* @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 AttributionSource attributionSource,
@NonNull ReadRecordsRequestParcel request,
@NonNull IReadRecordsResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, READ_DATA)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
throwExceptionIfDataSyncInProgress();
AtomicBoolean enforceSelfRead = new AtomicBoolean();
if (!holdsDataManagementPermission) {
boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
// If requesting app has only write permission allowed but no read
// permission for the record type or if app is not in foreground then
// allow to read its own records.
enforceSelfRead.set(
mDataPermissionEnforcer.enforceReadAccessAndGetEnforceSelfRead(
request.getRecordType(), attributionSource)
|| !isInForeground);
if (Constants.DEBUG) {
Slog.d(
TAG,
"Enforce self read for package "
+ attributionSource.getPackageName()
+ ":"
+ enforceSelfRead.get());
}
tryAcquireApiCallQuota(
uid,
QuotaCategory.QUOTA_CATEGORY_READ,
isInForeground,
builder);
}
final Map<String, Boolean> extraReadPermsToGrantState =
Collections.unmodifiableMap(
mDataPermissionEnforcer
.collectExtraReadPermissionToStateMapping(
Set.of(request.getRecordType()),
attributionSource));
Trace.traceBegin(TRACE_TAG_READ, TAG_READ);
try {
long startDateAccess = request.getStartTime();
if (!holdsDataManagementPermission) {
Instant startInstant =
mPermissionHelper.getHealthDataStartDateAccess(
attributionSource.getPackageName(), userHandle);
if (startInstant == null) {
throwExceptionIncorrectPermissionState();
}
// Always set the startDateAccess for local time filter, as for
// local date time we use it in conjunction with the time filter
// start-time
if (request.usesLocalTimeFilter()
|| startInstant.toEpochMilli() > startDateAccess) {
startDateAccess = startInstant.toEpochMilli();
}
}
Pair<List<RecordInternal<?>>, Long> readRecordsResponse =
mTransactionManager.readRecordsAndGetNextToken(
new ReadTransactionRequest(
attributionSource.getPackageName(),
request,
startDateAccess,
enforceSelfRead.get(),
extraReadPermsToGrantState));
builder.setNumberOfRecords(readRecordsResponse.first.size());
long pageToken =
request.getRecordIdFiltersParcel() == null
? readRecordsResponse.second
: DEFAULT_LONG;
if (pageToken != DEFAULT_LONG) {
// pagetoken is used here to store sorting order of the result.
// An even pagetoken indicate ascending and Odd page token indicate
// descending sort order. This detail from page token will be used
// in next read request to have same sort order.
pageToken =
request.isAscending() ? pageToken * 2 : pageToken * 2 + 1;
}
if (Constants.DEBUG) {
Slog.d(TAG, "pageToken: " + pageToken);
}
final String packageName = attributionSource.getPackageName();
final List<Integer> recordTypes =
Collections.singletonList(request.getRecordType());
// Calls from controller APK should not be recorded in access logs
// If an app is reading only its own data then it is not recorded in
// access logs.
boolean requiresLogging =
!holdsDataManagementPermission && !enforceSelfRead.get();
if (requiresLogging) {
Trace.traceBegin(
TRACE_TAG_READ_SUBTASKS, TAG_READ.concat("AddAccessLog"));
AccessLogsHelper.getInstance()
.addAccessLog(packageName, recordTypes, READ);
Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
}
callback.onResult(
new ReadRecordsResponseParcel(
new RecordsParcel(readRecordsResponse.first),
pageToken));
finishDataDeliveryRead(request.getRecordType(), attributionSource);
if (requiresLogging) {
logRecordTypeSpecificReadMetrics(
readRecordsResponse.first, packageName);
}
builder.setDataTypesFromRecordInternals(readRecordsResponse.first)
.setHealthDataServiceApiStatusSuccess();
} 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())) {
if (Constants.DEBUG) {
Slog.d(
TAG,
"No app info recorded for "
+ attributionSource.getPackageName());
}
callback.onResult(
new ReadRecordsResponseParcel(
new RecordsParcel(new ArrayList<>()),
DEFAULT_LONG));
builder.setHealthDataServiceApiStatusSuccess();
} else {
builder.setHealthDataServiceApiStatusError(
HealthConnectException.ERROR_UNKNOWN);
throw exception;
}
}
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (IllegalStateException illegalStateException) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "IllegalStateException: ", illegalStateException);
tryAndThrowException(callback, illegalStateException, ERROR_INTERNAL);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, ERROR_INTERNAL);
} finally {
Trace.traceEnd(TRACE_TAG_READ);
builder.build().log();
}
},
uid,
holdsDataManagementPermission);
}
/**
* 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 AttributionSource attributionSource,
@NonNull RecordsParcel recordsParcel,
@NonNull IEmptyResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(false, UPDATE_DATA)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
if (hasDataManagementPermission(uid, pid)) {
throw new SecurityException(
"Apps with android.permission.MANAGE_HEALTH_DATA permission are"
+ " not allowed to insert records");
}
enforceMemoryRateLimit(
recordsParcel.getRecordsSize(),
recordsParcel.getRecordsChunkSize());
final List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
builder.setNumberOfRecords(recordInternals.size());
throwExceptionIfDataSyncInProgress();
mDataPermissionEnforcer.enforceRecordsWritePermissions(
recordInternals, attributionSource);
boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
tryAcquireApiCallQuota(
uid, QuotaCategory.QUOTA_CATEGORY_WRITE, isInForeground, builder);
UpsertTransactionRequest request =
new UpsertTransactionRequest(
attributionSource.getPackageName(),
recordInternals,
mContext,
/* isInsertRequest */ false,
mDataPermissionEnforcer
.collectExtraWritePermissionStateMapping(
recordInternals, attributionSource));
mTransactionManager.updateAll(request);
tryAndReturnResult(callback, builder);
finishDataDeliveryWriteRecords(recordInternals, attributionSource);
logRecordTypeSpecificUpsertMetrics(
recordInternals, attributionSource.getPackageName());
builder.setDataTypesFromRecordInternals(recordInternals);
// Update activity dates table
HealthConnectThreadScheduler.scheduleInternalTask(
() ->
ActivityDateHelper.getInstance()
.reSyncByRecordTypeIds(
recordInternals.stream()
.map(RecordInternal::getRecordType)
.toList()));
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
Slog.e(TAG, "SqlException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (IllegalArgumentException illegalArgumentException) {
builder.setHealthDataServiceApiStatusError(
HealthConnectException.ERROR_INVALID_ARGUMENT);
Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_INVALID_ARGUMENT);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, ERROR_INTERNAL);
} finally {
builder.build().log();
}
},
uid,
false);
}
/**
* @see HealthConnectManager#getChangeLogToken
*/
@Override
public void getChangeLogToken(
@NonNull AttributionSource attributionSource,
@NonNull ChangeLogTokenRequest request,
@NonNull IGetChangeLogTokenCallback callback) {
final int uid = Binder.getCallingUid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(false, GET_CHANGES_TOKEN)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
tryAcquireApiCallQuota(
uid,
QuotaCategory.QUOTA_CATEGORY_READ,
mAppOpsManagerLocal.isUidInForeground(uid),
builder);
throwExceptionIfDataSyncInProgress();
mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
request.getRecordTypesList(), attributionSource);
callback.onResult(
new ChangeLogTokenResponse(
ChangeLogsRequestHelper.getInstance()
.getToken(
attributionSource.getPackageName(),
request)));
builder.setHealthDataServiceApiStatusSuccess();
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
tryAndThrowException(callback, e, ERROR_INTERNAL);
}
{
builder.build().log();
}
},
uid,
false);
}
/**
* @hide
* @see HealthConnectManager#getChangeLogs
*/
@Override
public void getChangeLogs(
@NonNull AttributionSource attributionSource,
@NonNull ChangeLogsRequest token,
IChangeLogsResponseCallback callback) {
final int uid = Binder.getCallingUid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final String callerPackageName = Objects.requireNonNull(attributionSource.getPackageName());
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(false, GET_CHANGES)
.setPackageName(callerPackageName);
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
throwExceptionIfDataSyncInProgress();
ChangeLogsRequestHelper.TokenRequest changeLogsTokenRequest =
ChangeLogsRequestHelper.getRequest(
attributionSource.getPackageName(), token.getToken());
mDataPermissionEnforcer.enforceRecordIdsReadPermissions(
changeLogsTokenRequest.getRecordTypes(), attributionSource);
boolean isInForeground = mAppOpsManagerLocal.isUidInForeground(uid);
if (!isInForeground) {
throwSecurityException(
attributionSource.getPackageName()
+ " must be in foreground to read the change logs");
}
tryAcquireApiCallQuota(
uid, QuotaCategory.QUOTA_CATEGORY_READ, isInForeground, builder);
Instant startDateInstant =
mPermissionHelper.getHealthDataStartDateAccess(
attributionSource.getPackageName(), userHandle);
if (startDateInstant == null) {
throwExceptionIncorrectPermissionState();
}
long startDateAccess = startDateInstant.toEpochMilli();
final ChangeLogsHelper.ChangeLogsResponse changeLogsResponse =
ChangeLogsHelper.getInstance()
.getChangeLogs(changeLogsTokenRequest, token);
Map<Integer, List<UUID>> recordTypeToInsertedUuids =
ChangeLogsHelper.getRecordTypeToInsertedUuids(
changeLogsResponse.getChangeLogsMap());
Map<String, Boolean> extraReadPermsToGrantState =
mDataPermissionEnforcer.collectExtraReadPermissionToStateMapping(
recordTypeToInsertedUuids.keySet(), attributionSource);
List<RecordInternal<?>> recordInternals =
mTransactionManager.readRecords(
new ReadTransactionRequest(
callerPackageName,
recordTypeToInsertedUuids,
startDateAccess,
extraReadPermsToGrantState));
List<DeletedLog> deletedLogs =
ChangeLogsHelper.getDeletedLogs(
changeLogsResponse.getChangeLogsMap());
callback.onResult(
new ChangeLogsResponse(
new RecordsParcel(recordInternals),
deletedLogs,
changeLogsResponse.getNextPageToken(),
changeLogsResponse.hasMorePages()));
finishDataDeliveryRead(
changeLogsTokenRequest.getRecordTypes(), attributionSource);
builder.setHealthDataServiceApiStatusSuccess()
.setNumberOfRecords(recordInternals.size() + deletedLogs.size())
.setDataTypesFromRecordInternals(recordInternals);
} catch (IllegalArgumentException illegalArgumentException) {
builder.setHealthDataServiceApiStatusError(
HealthConnectException.ERROR_INVALID_ARGUMENT);
Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_INVALID_ARGUMENT);
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (IllegalStateException illegalStateException) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "IllegalStateException: ", illegalStateException);
tryAndThrowException(callback, illegalStateException, ERROR_INTERNAL);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
} finally {
builder.build().log();
}
},
uid,
false);
}
/**
* API to delete records based on {@code request}
*
* <p>NOTE: Though internally we only need a single API to handle deletes as SDK code transform
* all its delete requests to {@link DeleteUsingFiltersRequestParcel}, we have this separation
* to make sure no non-controller APIs can use {@link
* HealthConnectServiceImpl#deleteUsingFilters} API
*/
@Override
public void deleteUsingFiltersForSelf(
@NonNull AttributionSource attributionSource,
@NonNull DeleteUsingFiltersRequestParcel request,
@NonNull IEmptyResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, DELETE_DATA)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
throwExceptionIfDataSyncInProgress();
List<Integer> recordTypeIdsToDelete =
(!request.getRecordTypeFilters().isEmpty())
? request.getRecordTypeFilters()
: new ArrayList<>(
RecordMapper.getInstance()
.getRecordIdToExternalRecordClassMap()
.keySet());
// Requests from non controller apps are not allowed to use non-id
// filters
request.setPackageNameFilters(
Collections.singletonList(attributionSource.getPackageName()));
if (!holdsDataManagementPermission) {
mDataPermissionEnforcer.enforceRecordIdsWritePermissions(
recordTypeIdsToDelete, attributionSource);
tryAcquireApiCallQuota(
uid,
QuotaCategory.QUOTA_CATEGORY_WRITE,
mAppOpsManagerLocal.isUidInForeground(uid),
builder);
}
deleteUsingFiltersInternal(
attributionSource,
request,
callback,
builder,
recordTypeIdsToDelete,
uid,
pid);
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (IllegalArgumentException illegalArgumentException) {
builder.setHealthDataServiceApiStatusError(
HealthConnectException.ERROR_INVALID_ARGUMENT);
Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_INVALID_ARGUMENT);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
} finally {
builder.build().log();
}
},
uid,
holdsDataManagementPermission);
}
/**
* API to delete records based on {@code request}
*
* <p>NOTE: Though internally we only need a single API to handle deletes as SDK code transform
* all its delete requests to {@link DeleteUsingFiltersRequestParcel}, we have this separation
* to make sure no non-controller APIs can use this API
*/
@Override
public void deleteUsingFilters(
@NonNull AttributionSource attributionSource,
@NonNull DeleteUsingFiltersRequestParcel request,
@NonNull IEmptyResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
final boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
final HealthConnectServiceLogger.Builder builder =
new HealthConnectServiceLogger.Builder(holdsDataManagementPermission, DELETE_DATA)
.setPackageName(attributionSource.getPackageName());
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
enforceIsForegroundUser(userHandle);
verifyPackageNameFromUid(uid, attributionSource);
throwExceptionIfDataSyncInProgress();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
List<Integer> recordTypeIdsToDelete =
(!request.getRecordTypeFilters().isEmpty())
? request.getRecordTypeFilters()
: new ArrayList<>(
RecordMapper.getInstance()
.getRecordIdToExternalRecordClassMap()
.keySet());
deleteUsingFiltersInternal(
attributionSource,
request,
callback,
builder,
recordTypeIdsToDelete,
uid,
pid);
} catch (SQLiteException sqLiteException) {
builder.setHealthDataServiceApiStatusError(HealthConnectException.ERROR_IO);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (IllegalArgumentException illegalArgumentException) {
builder.setHealthDataServiceApiStatusError(
HealthConnectException.ERROR_INVALID_ARGUMENT);
Slog.e(TAG, "IllegalArgumentException: ", illegalArgumentException);
tryAndThrowException(
callback,
illegalArgumentException,
HealthConnectException.ERROR_INVALID_ARGUMENT);
} catch (SecurityException securityException) {
builder.setHealthDataServiceApiStatusError(ERROR_SECURITY);
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
builder.setHealthDataServiceApiStatusError(
healthConnectException.getErrorCode());
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
} finally {
builder.build().log();
}
},
uid,
holdsDataManagementPermission);
}
private void deleteUsingFiltersInternal(
@NonNull AttributionSource attributionSource,
@NonNull DeleteUsingFiltersRequestParcel request,
@NonNull IEmptyResponseCallback callback,
@NonNull HealthConnectServiceLogger.Builder builder,
List<Integer> recordTypeIdsToDelete,
int uid,
int pid) {
if (request.usesIdFilters() && request.usesNonIdFilters()) {
throw new IllegalArgumentException(
"Requests with both id and non-id filters are not" + " supported");
}
int numberOfRecordsDeleted =
mTransactionManager.deleteAll(
new DeleteTransactionRequest(attributionSource.getPackageName(), request)
.setHasManageHealthDataPermission(
hasDataManagementPermission(uid, pid)));
tryAndReturnResult(callback, builder);
finishDataDeliveryWrite(recordTypeIdsToDelete, attributionSource);
HealthConnectThreadScheduler.scheduleInternalTask(
() -> postDeleteTasks(recordTypeIdsToDelete));
builder.setNumberOfRecords(numberOfRecordsDeleted)
.setDataTypesFromRecordTypes(recordTypeIdsToDelete);
}
/** API to get Priority for {@code dataCategory} */
@Override
public void getCurrentPriority(
@NonNull String packageName,
@HealthDataCategory.Type int dataCategory,
@NonNull IGetPriorityResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
List<DataOrigin> dataOriginInPriorityOrder =
HealthDataCategoryPriorityHelper.getInstance()
.getPriorityOrder(dataCategory)
.stream()
.map(
(name) ->
new DataOrigin.Builder()
.setPackageName(name)
.build())
.collect(Collectors.toList());
callback.onResult(
new GetPriorityResponseParcel(
new FetchDataOriginsPriorityOrderResponse(
dataOriginInPriorityOrder)));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
}
});
}
/** API to update priority for permission category(ies) */
@Override
public void updatePriority(
@NonNull String packageName,
@NonNull UpdatePriorityRequestParcel updatePriorityRequest,
@NonNull IEmptyResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
HealthDataCategoryPriorityHelper.getInstance()
.setPriorityOrder(
updatePriorityRequest.getDataCategory(),
updatePriorityRequest.getPackagePriorityOrder());
callback.onResult();
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
}
});
}
@Override
public void setRecordRetentionPeriodInDays(
int days, @NonNull UserHandle user, IEmptyResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
AutoDeleteService.setRecordRetentionPeriodInDays(days);
callback.onResult();
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SQLiteException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
}
});
}
@Override
public int getRecordRetentionPeriodInDays(@NonNull UserHandle user) {
enforceIsForegroundUser(getCallingUserHandle());
throwExceptionIfDataSyncInProgress();
try {
mContext.enforceCallingPermission(MANAGE_HEALTH_DATA_PERMISSION, null);
return AutoDeleteService.getRecordRetentionPeriodInDays();
} catch (Exception e) {
if (e instanceof SecurityException) {
throw e;
}
Slog.e(TAG, "Unable to get record retention period for " + user);
}
throw new RuntimeException();
}
/**
* 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) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
List<AppInfo> applicationInfos =
AppInfoHelper.getInstance().getApplicationInfosWithRecordTypes();
callback.onResult(new ApplicationInfoResponseParcel(applicationInfos));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SqlException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, ERROR_INTERNAL);
}
});
}
/** Retrieves {@link RecordTypeInfoResponse} for each RecordType. */
@Override
public void queryAllRecordTypesInfo(@NonNull IRecordTypeInfoResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
callback.onResult(
new RecordTypeInfoResponseParcel(
getPopulatedRecordTypeInfoResponses()));
} catch (SQLiteException sqLiteException) {
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
tryAndThrowException(callback, exception, ERROR_INTERNAL);
}
});
}
/**
* @see HealthConnectManager#queryAccessLogs
*/
@Override
public void queryAccessLogs(@NonNull String packageName, IAccessLogsResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
final List<AccessLog> accessLogsList =
AccessLogsHelper.getInstance().queryAccessLogs();
callback.onResult(new AccessLogsResponseParcel(accessLogsList));
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(callback, exception, ERROR_INTERNAL);
}
});
}
/**
* Returns a list of unique dates for which the database has at least one entry
*
* @param activityDatesRequestParcel Parcel request containing records classes
* @param callback Callback to receive result of performing this operation. The results are
* returned in {@link List<LocalDate>} . In case of an error or a permission failure the
* HealthConnect service, {@link IActivityDatesResponseCallback#onError} will be invoked
* with a {@link HealthConnectExceptionParcel}.
*/
@Override
public void getActivityDates(
@NonNull ActivityDatesRequestParcel activityDatesRequestParcel,
IActivityDatesResponseCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
throwExceptionIfDataSyncInProgress();
List<LocalDate> localDates =
ActivityDateHelper.getInstance()
.getActivityDates(
activityDatesRequestParcel.getRecordTypes());
callback.onResult(new ActivityDatesResponseParcel(localDates));
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "SqlException: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (SecurityException securityException) {
Slog.e(TAG, "SecurityException: ", securityException);
tryAndThrowException(callback, securityException, ERROR_SECURITY);
} catch (HealthConnectException healthConnectException) {
Slog.e(TAG, "HealthConnectException: ", healthConnectException);
tryAndThrowException(
callback,
healthConnectException,
healthConnectException.getErrorCode());
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, ERROR_INTERNAL);
}
});
}
// TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
// intent.
@Override
public void startMigration(@NonNull String packageName, IMigrationCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(
MIGRATE_HEALTH_CONNECT_DATA,
pid,
uid,
"Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
enforceShowMigrationInfoIntent(packageName, uid);
mBackupRestore.runWithStatesReadLock(
() -> {
if (mBackupRestore.isRestoreMergingInProgress()) {
throw new MigrationException(
"Cannot start data migration. Backup and restore in"
+ " progress.",
MigrationException.ERROR_INTERNAL,
null);
}
mMigrationStateManager.startMigration(mContext);
});
PriorityMigrationHelper.getInstance().populatePreMigrationPriority();
callback.onSuccess();
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
}
});
}
// TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
// intent.
@Override
public void finishMigration(@NonNull String packageName, IMigrationCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(
MIGRATE_HEALTH_CONNECT_DATA,
pid,
uid,
"Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
enforceShowMigrationInfoIntent(packageName, uid);
mMigrationStateManager.finishMigration(mContext);
AppInfoHelper.getInstance().syncAppInfoRecordTypesUsed();
callback.onSuccess();
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
// TODO(b/263897830): Verify migration state and send errors properly
tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
}
});
}
// TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
// intent.
@Override
public void writeMigrationData(
@NonNull String packageName,
MigrationEntityParcel parcel,
IMigrationCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
UserHandle callingUserHandle = getCallingUserHandle();
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
enforceIsForegroundUser(callingUserHandle);
mContext.enforcePermission(
MIGRATE_HEALTH_CONNECT_DATA,
pid,
uid,
"Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
enforceShowMigrationInfoIntent(packageName, uid);
mMigrationStateManager.validateWriteMigrationData();
getDataMigrationManager(callingUserHandle)
.apply(parcel.getMigrationEntities());
callback.onSuccess();
} catch (DataMigrationManager.EntityWriteException e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(
callback,
e,
MigrationException.ERROR_MIGRATE_ENTITY,
e.getEntityId());
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
}
});
}
public void insertMinDataMigrationSdkExtensionVersion(
@NonNull String packageName, int requiredSdkExtension, IMigrationCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(
MIGRATE_HEALTH_CONNECT_DATA,
pid,
uid,
"Caller does not have " + MIGRATE_HEALTH_CONNECT_DATA);
enforceShowMigrationInfoIntent(packageName, uid);
mMigrationStateManager.validateSetMinSdkVersion();
mMigrationStateManager.setMinDataMigrationSdkExtensionVersion(
mContext, requiredSdkExtension);
callback.onSuccess();
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, MigrationException.ERROR_INTERNAL, null);
}
});
}
/**
* @see HealthConnectManager#stageAllHealthConnectRemoteData
*/
@Override
public void stageAllHealthConnectRemoteData(
@NonNull StageRemoteDataRequest stageRemoteDataRequest,
@NonNull UserHandle userHandle,
@NonNull IDataStagingFinishedCallback callback) {
Map<String, ParcelFileDescriptor> origPfdsByFileName =
stageRemoteDataRequest.getPfdsByFileName();
Map<String, HealthConnectException> exceptionsByFileName =
new ArrayMap<>(origPfdsByFileName.size());
Map<String, ParcelFileDescriptor> pfdsByFileName =
new ArrayMap<>(origPfdsByFileName.size());
try {
mDataPermissionEnforcer.enforceAnyOfPermissions(
Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA,
HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION);
enforceIsForegroundUser(Binder.getCallingUserHandle());
for (Entry<String, ParcelFileDescriptor> entry : origPfdsByFileName.entrySet()) {
try {
pfdsByFileName.put(entry.getKey(), entry.getValue().dup());
} catch (IOException e) {
Slog.e(TAG, "IOException: ", e);
exceptionsByFileName.put(
entry.getKey(),
new HealthConnectException(
HealthConnectException.ERROR_IO, e.getMessage()));
}
}
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
if (!mBackupRestore.prepForStagingIfNotAlreadyDone()) {
try {
callback.onResult();
} catch (RemoteException e) {
Log.e(TAG, "Restore response could not be sent to the caller.", e);
}
return;
}
mBackupRestore.stageAllHealthConnectRemoteData(
pfdsByFileName,
exceptionsByFileName,
userHandle.getIdentifier(),
callback);
});
} catch (SecurityException | IllegalStateException e) {
Log.e(TAG, "Exception encountered while staging", e);
try {
@HealthConnectException.ErrorCode
int errorCode = (e instanceof SecurityException) ? ERROR_SECURITY : ERROR_INTERNAL;
exceptionsByFileName.put("", new HealthConnectException(errorCode, e.getMessage()));
callback.onError(new StageRemoteDataException(exceptionsByFileName));
} catch (RemoteException remoteException) {
Log.e(TAG, "Restore permission response could not be sent to the caller.", e);
}
}
}
/**
* @see HealthConnectManager#getAllDataForBackup
*/
@Override
public void getAllDataForBackup(
@NonNull StageRemoteDataRequest stageRemoteDataRequest,
@NonNull UserHandle userHandle) {
mContext.enforceCallingPermission(HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION, null);
mBackupRestore.getAllDataForBackup(stageRemoteDataRequest, userHandle);
}
/**
* @see HealthConnectManager#getAllBackupFileNames
*/
@Override
public BackupFileNamesSet getAllBackupFileNames(boolean forDeviceToDevice) {
mContext.enforceCallingPermission(HEALTH_CONNECT_BACKUP_INTER_AGENT_PERMISSION, null);
return mBackupRestore.getAllBackupFileNames(forDeviceToDevice);
}
/**
* @see HealthConnectManager#deleteAllStagedRemoteData
*/
@Override
public void deleteAllStagedRemoteData(@NonNull UserHandle userHandle) {
mContext.enforceCallingPermission(
DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA_PERMISSION, null);
mBackupRestore.deleteAndResetEverything(userHandle);
mMigrationStateManager.clearCaches(mContext);
AppInfoHelper.getInstance().clearData(mTransactionManager);
ActivityDateHelper.getInstance().clearData(mTransactionManager);
MigrationEntityHelper.getInstance().clearData(mTransactionManager);
HealthDataCategoryPriorityHelper.getInstance().clearData(mTransactionManager);
PriorityMigrationHelper.getInstance().clearData(mTransactionManager);
RateLimiter.clearCache();
String[] packageNames = mContext.getPackageManager().getPackagesForUid(getCallingUid());
for (String packageName : packageNames) {
mFirstGrantTimeManager.setFirstGrantTime(packageName, Instant.now(), userHandle);
}
}
/**
* @see HealthConnectManager#updateDataDownloadState
*/
@Override
public void updateDataDownloadState(@DataDownloadState int downloadState) {
mContext.enforceCallingPermission(
Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA, null);
enforceIsForegroundUser(getCallingUserHandle());
mBackupRestore.updateDataDownloadState(downloadState);
}
/**
* @see HealthConnectManager#getHealthConnectDataState
*/
@Override
public void getHealthConnectDataState(@NonNull IGetHealthConnectDataStateCallback callback) {
try {
mDataPermissionEnforcer.enforceAnyOfPermissions(
MANAGE_HEALTH_DATA_PERMISSION, Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
final UserHandle userHandle = Binder.getCallingUserHandle();
enforceIsForegroundUser(userHandle);
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
@HealthConnectDataState.DataRestoreError
int dataRestoreError = mBackupRestore.getDataRestoreError();
@HealthConnectDataState.DataRestoreState
int dataRestoreState = mBackupRestore.getDataRestoreState();
try {
callback.onResult(
new HealthConnectDataState(
dataRestoreState,
dataRestoreError,
mMigrationStateManager.getMigrationState()));
} catch (RemoteException remoteException) {
Log.e(
TAG,
"HealthConnectDataState could not be sent to the caller.",
remoteException);
}
} catch (RuntimeException e) {
// exception getting the state from the disk
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(
HealthConnectException.ERROR_IO,
e.getMessage())));
} catch (RemoteException remoteException) {
Log.e(
TAG,
"Exception for getHealthConnectDataState could not be sent"
+ " to the caller.",
remoteException);
}
}
});
} catch (SecurityException | IllegalStateException e) {
Log.e(TAG, "getHealthConnectDataState: Exception encountered", e);
@HealthConnectException.ErrorCode
int errorCode = (e instanceof SecurityException) ? ERROR_SECURITY : ERROR_INTERNAL;
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(errorCode, e.getMessage())));
} catch (RemoteException remoteException) {
Log.e(TAG, "getHealthConnectDataState error could not be sent", e);
}
}
}
/**
* @see HealthConnectManager#getHealthConnectMigrationUiState
*/
@Override
public void getHealthConnectMigrationUiState(
@NonNull IGetHealthConnectMigrationUiStateCallback callback) {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
final UserHandle userHandle = Binder.getCallingUserHandle();
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
enforceIsForegroundUser(userHandle);
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
try {
callback.onResult(
new HealthConnectMigrationUiState(
mMigrationUiStateManager
.getHealthConnectMigrationUiState()));
} catch (RemoteException remoteException) {
Log.e(
TAG,
"HealthConnectMigrationUiState could not be sent to the"
+ " caller.",
remoteException);
}
} catch (SecurityException securityException) {
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(
ERROR_SECURITY,
securityException.getMessage())));
} catch (RemoteException remoteException) {
Log.e(
TAG,
"Exception for HealthConnectMigrationUiState could not be sent"
+ " to the caller.",
remoteException);
}
} catch (RuntimeException e) {
// exception getting the state from the disk
try {
callback.onError(
new HealthConnectExceptionParcel(
new HealthConnectException(
HealthConnectException.ERROR_IO,
e.getMessage())));
} catch (RemoteException remoteException) {
Log.e(
TAG,
"Exception for HealthConnectMigrationUiState could not be sent"
+ " to the caller.",
remoteException);
}
}
});
}
// Cancel BR timeouts - this might be needed when a user is going into background.
void cancelBackupRestoreTimeouts() {
mBackupRestore.cancelAllJobs();
}
private void tryAcquireApiCallQuota(
int uid,
@QuotaCategory.Type int quotaCategory,
boolean isInForeground,
HealthConnectServiceLogger.Builder builder) {
try {
RateLimiter.tryAcquireApiCallQuota(uid, quotaCategory, isInForeground);
} catch (RateLimiterException rateLimiterException) {
builder.setRateLimit(
rateLimiterException.getRateLimiterQuotaBucket(),
rateLimiterException.getRateLimiterQuotaLimit());
throw new HealthConnectException(
rateLimiterException.getErrorCode(), rateLimiterException.getMessage());
}
}
private void enforceMemoryRateLimit(List<Long> recordsSize, long recordsChunkSize) {
recordsSize.forEach(RateLimiter::checkMaxRecordMemoryUsage);
RateLimiter.checkMaxChunkMemoryUsage(recordsChunkSize);
}
private void enforceIsForegroundUser(UserHandle callingUserHandle) {
if (!callingUserHandle.equals(mCurrentForegroundUser)) {
throw new IllegalStateException(
"Calling user: "
+ callingUserHandle.getIdentifier()
+ "is not the current foreground user: "
+ mCurrentForegroundUser.getIdentifier()
+ ". HC request must be called"
+ " from the current foreground user.");
}
}
private boolean isDataSyncInProgress() {
return mMigrationStateManager.isMigrationInProgress()
|| mBackupRestore.isRestoreMergingInProgress();
}
@VisibleForTesting
Set<String> getStagedRemoteFileNames(int userId) {
return mBackupRestore.getStagedRemoteFileNames(userId);
}
@NonNull
private DataMigrationManager getDataMigrationManager(@NonNull UserHandle userHandle) {
final Context userContext = mContext.createContextAsUser(userHandle, 0);
return new DataMigrationManager(
userContext,
mTransactionManager,
mPermissionHelper,
mFirstGrantTimeManager,
DeviceInfoHelper.getInstance(),
AppInfoHelper.getInstance(),
MigrationEntityHelper.getInstance(),
RecordHelperProvider.getInstance(),
HealthDataCategoryPriorityHelper.getInstance(),
PriorityMigrationHelper.getInstance(),
ActivityDateHelper.getInstance());
}
private void enforceCallingPackageBelongsToUid(String packageName, int callingUid) {
int packageUid;
try {
packageUid =
mContext.getPackageManager()
.getPackageUid(
packageName, /* flags */ PackageManager.PackageInfoFlags.of(0));
} catch (PackageManager.NameNotFoundException e) {
throw new IllegalStateException(packageName + " not found");
}
if (UserHandle.getAppId(packageUid) != UserHandle.getAppId(callingUid)) {
throwSecurityException(packageName + " does not belong to uid " + callingUid);
}
}
/**
* Verify various aspects of the calling user.
*
* @param callingUid Uid of the caller, usually retrieved from Binder for authenticity.
* @param callerAttributionSource The permission identity of the caller
*/
private void verifyPackageNameFromUid(
int callingUid, @NonNull AttributionSource callerAttributionSource) {
// Check does the attribution source is one for the calling app.
callerAttributionSource.enforceCallingUid();
// Obtain the user where the client is running in.
UserHandle callingUserHandle = UserHandle.getUserHandleForUid(callingUid);
Context callingUserContext = mContext.createContextAsUser(callingUserHandle, 0);
String callingPackageName =
Objects.requireNonNull(callerAttributionSource.getPackageName());
verifyCallingPackage(callingUserContext, callingUid, callingPackageName);
}
/**
* Check that the caller's supposed package name matches the uid making the call.
*
* @throws SecurityException if the package name and uid don't match.
*/
private void verifyCallingPackage(
@NonNull Context actualCallingUserContext,
int actualCallingUid,
@NonNull String claimedCallingPackage) {
int claimedCallingUid = getPackageUid(actualCallingUserContext, claimedCallingPackage);
if (claimedCallingUid != actualCallingUid) {
throwSecurityException(
claimedCallingPackage + " does not belong to uid " + actualCallingUid);
}
}
/** Finds the UID of the {@code packageName} in the given {@code context}. */
private int getPackageUid(@NonNull Context context, @NonNull String packageName) {
try {
return context.getPackageManager().getPackageUid(packageName, /* flags= */ 0);
} catch (PackageManager.NameNotFoundException e) {
return Process.INVALID_UID;
}
}
private void enforceShowMigrationInfoIntent(String packageName, int callingUid) {
enforceCallingPackageBelongsToUid(packageName, callingUid);
Intent intentToCheck =
new Intent(HealthConnectManager.ACTION_SHOW_MIGRATION_INFO).setPackage(packageName);
ResolveInfo resolveResult =
mContext.getPackageManager()
.resolveActivity(
intentToCheck,
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_ALL));
if (Objects.isNull(resolveResult)) {
throw new IllegalArgumentException(
packageName
+ " does not handle intent "
+ HealthConnectManager.ACTION_SHOW_MIGRATION_INFO);
}
}
private Map<Integer, List<DataOrigin>> getPopulatedRecordTypeInfoResponses() {
Map<Integer, Class<? extends Record>> recordIdToExternalRecordClassMap =
RecordMapper.getInstance().getRecordIdToExternalRecordClassMap();
AppInfoHelper appInfoHelper = AppInfoHelper.getInstance();
Map<Integer, List<DataOrigin>> recordTypeInfoResponses =
new ArrayMap<>(recordIdToExternalRecordClassMap.size());
Map<Integer, Set<String>> recordTypeToContributingPackagesMap =
appInfoHelper.getRecordTypesToContributingPackagesMap();
recordIdToExternalRecordClassMap
.keySet()
.forEach(
(recordType) -> {
if (recordTypeToContributingPackagesMap.containsKey(recordType)) {
List<DataOrigin> packages =
recordTypeToContributingPackagesMap.get(recordType).stream()
.map(
(packageName) ->
new DataOrigin.Builder()
.setPackageName(packageName)
.build())
.toList();
recordTypeInfoResponses.put(recordType, packages);
} else {
recordTypeInfoResponses.put(recordType, Collections.emptyList());
}
});
return recordTypeInfoResponses;
}
private boolean hasDataManagementPermission(int uid, int pid) {
return mContext.checkPermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid)
== PERMISSION_GRANTED;
}
private void finishDataDeliveryRead(int recordTypeId, AttributionSource attributionSource) {
finishDataDeliveryRead(Collections.singletonList(recordTypeId), attributionSource);
}
private void finishDataDeliveryRead(
List<Integer> recordTypeIds, AttributionSource attributionSource) {
Trace.traceBegin(TRACE_TAG_READ_SUBTASKS, TAG_READ.concat("FinishDataDeliveryRead"));
try {
for (Integer recordTypeId : recordTypeIds) {
String permissionName =
HealthPermissions.getHealthReadPermission(
RecordTypePermissionCategoryMapper
.getHealthPermissionCategoryForRecordType(recordTypeId));
mPermissionManager.finishDataDelivery(permissionName, attributionSource);
}
} catch (Exception exception) {
// Ignore: HC API has already fulfilled the result, ignore any exception we hit here
}
Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
}
private void finishDataDeliveryWriteRecords(
List<RecordInternal<?>> recordInternals, AttributionSource attributionSource) {
Trace.traceBegin(TRACE_TAG_READ_SUBTASKS, TAG_READ.concat(".FinishDataDeliveryWrite"));
Set<Integer> recordTypeIdsToEnforce = new ArraySet<>();
for (RecordInternal<?> recordInternal : recordInternals) {
recordTypeIdsToEnforce.add(recordInternal.getRecordType());
}
finishDataDeliveryWrite(recordTypeIdsToEnforce.stream().toList(), attributionSource);
Trace.traceEnd(TRACE_TAG_READ_SUBTASKS);
}
private void finishDataDeliveryWrite(
List<Integer> recordTypeIds, AttributionSource attributionSource) {
try {
for (Integer recordTypeId : recordTypeIds) {
String permissionName =
HealthPermissions.getHealthWritePermission(
RecordTypePermissionCategoryMapper
.getHealthPermissionCategoryForRecordType(recordTypeId));
mPermissionManager.finishDataDelivery(permissionName, attributionSource);
}
} catch (Exception exception) {
// Ignore: HC API has already fulfilled the result, ignore any exception we hit here
}
}
private void enforceBinderUidIsSameAsAttributionSourceUid(
int binderUid, int attributionSourceUid) {
if (binderUid != attributionSourceUid) {
throw new SecurityException("Binder uid must be equal to attribution source uid.");
}
}
private void throwExceptionIncorrectPermissionState() {
throw new IllegalStateException(
"Incorrect health permission state, likely"
+ " because the calling application's manifest does not specify handling "
+ Intent.ACTION_VIEW_PERMISSION_USAGE
+ " with "
+ HealthConnectManager.CATEGORY_HEALTH_PERMISSIONS);
}
private void logRecordTypeSpecificUpsertMetrics(
@NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
Objects.requireNonNull(recordInternals);
Objects.requireNonNull(packageName);
Map<Integer, List<RecordInternal<?>>> recordTypeToRecordInternals =
getRecordTypeToListOfRecords(recordInternals);
for (Entry<Integer, List<RecordInternal<?>>> recordTypeToRecordInternalsEntry :
recordTypeToRecordInternals.entrySet()) {
RecordHelper<?> recordHelper =
RecordHelperProvider.getInstance()
.getRecordHelper(recordTypeToRecordInternalsEntry.getKey());
recordHelper.logUpsertMetrics(recordTypeToRecordInternalsEntry.getValue(), packageName);
}
}
private void logRecordTypeSpecificReadMetrics(
@NonNull List<RecordInternal<?>> recordInternals, @NonNull String packageName) {
Objects.requireNonNull(recordInternals);
Objects.requireNonNull(packageName);
Map<Integer, List<RecordInternal<?>>> recordTypeToRecordInternals =
getRecordTypeToListOfRecords(recordInternals);
for (Entry<Integer, List<RecordInternal<?>>> recordTypeToRecordInternalsEntry :
recordTypeToRecordInternals.entrySet()) {
RecordHelper<?> recordHelper =
RecordHelperProvider.getInstance()
.getRecordHelper(recordTypeToRecordInternalsEntry.getKey());
recordHelper.logReadMetrics(recordTypeToRecordInternalsEntry.getValue(), packageName);
}
}
private Map<Integer, List<RecordInternal<?>>> getRecordTypeToListOfRecords(
List<RecordInternal<?>> recordInternals) {
return recordInternals.stream()
.collect(Collectors.groupingBy(RecordInternal::getRecordType));
}
private void throwSecurityException(String message) {
throw new SecurityException(message);
}
private void throwExceptionIfDataSyncInProgress() {
if (isDataSyncInProgress()) {
throw new HealthConnectException(
HealthConnectException.ERROR_DATA_SYNC_IN_PROGRESS,
"Storage data sync in progress. API calls are blocked");
}
}
/**
* Throws an IllegalState Exception if data migration or restore is in process. This is only
* used by HealthConnect synchronous APIs as {@link HealthConnectException} is lost between
* processes on synchronous APIs and can only be returned to the caller for the APIs with a
* callback.
*/
private void throwIllegalStateExceptionIfDataSyncInProgress() {
if (isDataSyncInProgress()) {
throw new IllegalStateException("Storage data sync in progress. API calls are blocked");
}
}
private static void postDeleteTasks(List<Integer> recordTypeIdsToDelete) {
Trace.traceBegin(TRACE_TAG_DELETE_SUBTASKS, TAG_INSERT.concat("PostDeleteTasks"));
if (recordTypeIdsToDelete != null && !recordTypeIdsToDelete.isEmpty()) {
AppInfoHelper.getInstance()
.syncAppInfoRecordTypesUsed(new HashSet<>(recordTypeIdsToDelete));
ActivityDateHelper.getInstance().reSyncByRecordTypeIds(recordTypeIdsToDelete);
}
Trace.traceEnd(TRACE_TAG_DELETE_SUBTASKS);
}
private static void tryAndReturnResult(
IEmptyResponseCallback callback, HealthConnectServiceLogger.Builder builder) {
try {
callback.onResult();
builder.setHealthDataServiceApiStatusSuccess();
} catch (RemoteException e) {
Slog.e(TAG, "Remote call failed", e);
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
}
}
private static void tryAndReturnResult(
IInsertRecordsResponseCallback callback,
List<String> uuids,
HealthConnectServiceLogger.Builder builder) {
try {
callback.onResult(new InsertRecordsResponseParcel(uuids));
builder.setHealthDataServiceApiStatusSuccess();
} catch (RemoteException e) {
Slog.e(TAG, "Remote call failed", e);
builder.setHealthDataServiceApiStatusError(ERROR_INTERNAL);
}
}
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);
}
}
private static void tryAndThrowException(
@NonNull IAggregateRecordsResponseCallback 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 IReadRecordsResponseCallback 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 IActivityDatesResponseCallback 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 IGetChangeLogTokenCallback 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 IAccessLogsResponseCallback 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 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.toString())));
} 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.toString())));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
private static void tryAndThrowException(
@NonNull IGetPriorityResponseCallback 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 IMigrationCallback callback,
@NonNull Exception exception,
@MigrationException.ErrorCode int errorCode,
@Nullable String failedEntityId) {
try {
callback.onError(
new MigrationException(exception.toString(), errorCode, failedEntityId));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
}