blob: 0822140be3ce1854338026019e3f3c9c23383cbc [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.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.healthconnect.Constants.DEFAULT_LONG;
import static android.healthconnect.Constants.READ;
import static android.healthconnect.HealthConnectManager.DATA_DOWNLOAD_COMPLETE;
import static android.healthconnect.HealthConnectManager.DATA_DOWNLOAD_FAILED;
import static android.healthconnect.HealthConnectManager.DATA_DOWNLOAD_STATE_UNKNOWN;
import static android.healthconnect.HealthPermissions.MANAGE_HEALTH_DATA_PERMISSION;
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.AttributionSource;
import android.content.Context;
import android.database.sqlite.SQLiteException;
import android.healthconnect.AccessLog;
import android.healthconnect.Constants;
import android.healthconnect.FetchDataOriginsPriorityOrderResponse;
import android.healthconnect.HealthConnectException;
import android.healthconnect.HealthConnectManager;
import android.healthconnect.HealthConnectManager.DataDownloadState;
import android.healthconnect.HealthDataCategory;
import android.healthconnect.HealthPermissions;
import android.healthconnect.aidl.AccessLogsResponseParcel;
import android.healthconnect.aidl.ActivityDatesRequestParcel;
import android.healthconnect.aidl.ActivityDatesResponseParcel;
import android.healthconnect.aidl.AggregateDataRequestParcel;
import android.healthconnect.aidl.ApplicationInfoResponseParcel;
import android.healthconnect.aidl.ChangeLogTokenRequestParcel;
import android.healthconnect.aidl.ChangeLogTokenResponseParcel;
import android.healthconnect.aidl.ChangeLogsRequestParcel;
import android.healthconnect.aidl.ChangeLogsResponseParcel;
import android.healthconnect.aidl.DeleteUsingFiltersRequestParcel;
import android.healthconnect.aidl.GetPriorityResponseParcel;
import android.healthconnect.aidl.HealthConnectExceptionParcel;
import android.healthconnect.aidl.IAccessLogsResponseCallback;
import android.healthconnect.aidl.IActivityDatesResponseCallback;
import android.healthconnect.aidl.IAggregateRecordsResponseCallback;
import android.healthconnect.aidl.IApplicationInfoResponseCallback;
import android.healthconnect.aidl.IChangeLogsResponseCallback;
import android.healthconnect.aidl.IDataStagingFinishedCallback;
import android.healthconnect.aidl.IEmptyResponseCallback;
import android.healthconnect.aidl.IGetChangeLogTokenCallback;
import android.healthconnect.aidl.IGetPriorityResponseCallback;
import android.healthconnect.aidl.IHealthConnectService;
import android.healthconnect.aidl.IInsertRecordsResponseCallback;
import android.healthconnect.aidl.IMigrationCallback;
import android.healthconnect.aidl.IReadRecordsResponseCallback;
import android.healthconnect.aidl.IRecordTypeInfoResponseCallback;
import android.healthconnect.aidl.InsertRecordsResponseParcel;
import android.healthconnect.aidl.ReadRecordsRequestParcel;
import android.healthconnect.aidl.ReadRecordsResponseParcel;
import android.healthconnect.aidl.RecordIdFiltersParcel;
import android.healthconnect.aidl.RecordTypeInfoResponseParcel;
import android.healthconnect.aidl.RecordsParcel;
import android.healthconnect.aidl.UpdatePriorityRequestParcel;
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.AggregationTypeIdMapper;
import android.healthconnect.internal.datatypes.utils.RecordMapper;
import android.healthconnect.internal.datatypes.utils.RecordTypePermissionCategoryMapper;
import android.healthconnect.migration.MigrationEntity;
import android.healthconnect.migration.MigrationException;
import android.healthconnect.restore.StageRemoteDataException;
import android.healthconnect.restore.StageRemoteDataRequest;
import android.os.Binder;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
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.healthconnect.migration.DataMigrationManager;
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.PreferenceHelper;
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.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Instant;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* IHealthConnectService's implementation
*
* @hide
*/
final class HealthConnectServiceImpl extends IHealthConnectService.Stub {
// Key for storing the current data download state
@VisibleForTesting static final String DATA_DOWNLOAD_STATE_KEY = "DATA_DOWNLOAD_STATE_KEY";
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";
// Key for storing the current data restore state on disk.
private static final String DATA_RESTORE_STATE_KEY = "DATA_RESTORE_STATE_KEY";
// Key for storing whether there was any error during the whole data "restore" phase.
// The "restore" phase includes downloading, staging, and merging.
private static final String WAS_DATA_RESTORE_ERROR_ENCOUNTERED =
"WAS_DATA_RESTORE_ERROR_ENCOUNTERED";
@Retention(RetentionPolicy.SOURCE)
@IntDef({
DATA_RESTORE_STATE_UNKNOWN,
DATA_RESTORE_WAITING_FOR_STAGING,
DATA_RESTORE_STAGING_IN_PROGRESS,
DATA_RESTORE_STAGING_DONE,
DATA_RESTORE_MERGING_IN_PROGRESS,
DATA_RESTORE_MERGING_DONE
})
@interface DataRestoreState {}
// The below values for the IntDef are defined in chronological order of the restore process.
static final int DATA_RESTORE_STATE_UNKNOWN = 0;
static final int DATA_RESTORE_WAITING_FOR_STAGING = 1;
static final int DATA_RESTORE_STAGING_IN_PROGRESS = 2;
static final int DATA_RESTORE_STAGING_DONE = 3;
static final int DATA_RESTORE_MERGING_IN_PROGRESS = 4;
static final int DATA_RESTORE_MERGING_DONE = 5;
private final TransactionManager mTransactionManager;
private final HealthConnectPermissionHelper mPermissionHelper;
private final FirstGrantTimeManager mFirstGrantTimeManager;
private final Context mContext;
private final PermissionManager mPermissionManager;
HealthConnectServiceImpl(
TransactionManager transactionManager,
HealthConnectPermissionHelper permissionHelper,
FirstGrantTimeManager firstGrantTimeManager,
Context context) {
mTransactionManager = transactionManager;
mPermissionHelper = permissionHelper;
mFirstGrantTimeManager = firstGrantTimeManager;
mContext = context;
mPermissionManager = mContext.getSystemService(PermissionManager.class);
}
@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);
}
@Override
public long getHistoricalAccessStartDateInMilliseconds(
@NonNull String packageName, @NonNull UserHandle userHandle) {
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 String packageName,
@NonNull RecordsParcel recordsParcel,
@NonNull IInsertRecordsResponseCallback callback) {
List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
int uid = Binder.getCallingUid();
enforceRecordWritePermissionForRecords(recordInternals, uid);
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
List<String> uuids =
mTransactionManager.insertAll(
new UpsertTransactionRequest(
packageName,
recordInternals,
mContext,
/* isInsertRequest */ true));
callback.onResult(new InsertRecordsResponseParcel(uuids));
HealthConnectThreadScheduler.scheduleInternalTask(
() ->
ActivityDateHelper.getInstance()
.insertRecordDate(recordsParcel.getRecords()));
finishDataDeliveryWriteRecords(recordInternals, uid);
} 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);
}
},
uid,
false);
}
/**
* 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) {
List<Integer> recordTypesToTest = new ArrayList<>();
for (int aggregateId : request.getAggregateIds()) {
recordTypesToTest.addAll(
AggregationTypeIdMapper.getInstance()
.getAggregationTypeFor(aggregateId)
.getApplicableRecordTypeIds());
}
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
if (!holdsDataManagementPermission) {
enforceRecordReadPermission(recordTypesToTest, uid);
}
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
callback.onResult(
new AggregateTransactionRequest(packageName, request)
.getAggregateDataResponseParcel());
finishDataDeliveryRead(recordTypesToTest, uid);
} 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);
}
},
uid,
holdsDataManagementPermission);
}
/**
* 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) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
AtomicBoolean enforceSelfRead = new AtomicBoolean();
if (!holdsDataManagementPermission) {
enforceSelfRead.set(enforceRecordReadPermission(uid, request.getRecordType()));
}
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
try {
Pair<List<RecordInternal<?>>, Long> readRecordsResponse =
mTransactionManager.readRecordsAndGetNextToken(
new ReadTransactionRequest(
packageName, request, enforceSelfRead.get()));
long pageToken =
request.getRecordIdFiltersParcel() == null
? readRecordsResponse.second
: DEFAULT_LONG;
if (Constants.DEBUG) {
Slog.d(TAG, "pageToken: " + pageToken);
}
callback.onResult(
new ReadRecordsResponseParcel(
new RecordsParcel(readRecordsResponse.first),
pageToken));
// Calls from controller APK should not be recorded in access logs.
if (!holdsDataManagementPermission) {
HealthConnectThreadScheduler.scheduleInternalTask(
() ->
AccessLogsHelper.getInstance()
.addAccessLog(
packageName,
Collections.singletonList(
request.getRecordType()),
READ));
}
finishDataDeliveryRead(request.getRecordType(), uid);
} 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 ReadRecordsResponseParcel(
new RecordsParcel(new ArrayList<>()),
DEFAULT_LONG));
} else {
throw exception;
}
}
} catch (SQLiteException sqLiteException) {
Slog.e(TAG, "Exception: ", sqLiteException);
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
},
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 String packageName,
@NonNull RecordsParcel recordsParcel,
@NonNull IEmptyResponseCallback callback) {
int uid = Binder.getCallingUid();
List<RecordInternal<?>> recordInternals = recordsParcel.getRecords();
enforceRecordWritePermissionForRecords(recordInternals, uid);
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
mTransactionManager.updateAll(
new UpsertTransactionRequest(
packageName,
recordInternals,
mContext,
/* isInsertRequest */ false));
callback.onResult();
finishDataDeliveryWriteRecords(recordInternals, uid);
} catch (SecurityException securityException) {
tryAndThrowException(
callback, securityException, HealthConnectException.ERROR_SECURITY);
} 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);
}
},
uid,
false);
}
/**
* @see HealthConnectManager#getChangeLogToken
*/
@Override
public void getChangeLogToken(
@NonNull String packageName,
@NonNull ChangeLogTokenRequestParcel request,
@NonNull IGetChangeLogTokenCallback callback) {
int uid = Binder.getCallingUid();
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
callback.onResult(
new ChangeLogTokenResponseParcel(
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);
}
},
uid,
false);
}
/**
* @hide
* @see HealthConnectManager#getChangeLogs
*/
@Override
public void getChangeLogs(
@NonNull String packageName,
@NonNull ChangeLogsRequestParcel token,
IChangeLogsResponseCallback callback) {
int uid = Binder.getCallingUid();
ChangeLogsRequestHelper.TokenRequest changeLogsTokenRequest =
ChangeLogsRequestHelper.getRequest(packageName, token.getToken());
enforceRecordReadPermission(changeLogsTokenRequest.getRecordTypes(), uid);
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
final ChangeLogsHelper.ChangeLogsResponse changeLogsResponse =
ChangeLogsHelper.getInstance()
.getChangeLogs(changeLogsTokenRequest, token.getPageSize());
List<RecordInternal<?>> recordInternals =
mTransactionManager.readRecords(
new ReadTransactionRequest(
ChangeLogsHelper.getRecordTypeToInsertedUuids(
changeLogsResponse.getChangeLogsMap())));
callback.onResult(
new ChangeLogsResponseParcel(
new RecordsParcel(recordInternals),
ChangeLogsHelper.getDeletedIds(
changeLogsResponse.getChangeLogsMap()),
changeLogsResponse.getNextPageToken(),
changeLogsResponse.hasMorePages()));
finishDataDeliveryRead(changeLogsTokenRequest.getRecordTypes(), uid);
} 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);
}
},
uid,
false);
}
/**
* 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) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
boolean holdsDataManagementPermission = hasDataManagementPermission(uid, pid);
List<Integer> recordTypeIdsToDelete =
(!request.getRecordTypeFilters().isEmpty())
? request.getRecordTypeFilters()
: new ArrayList<>(
RecordMapper.getInstance()
.getRecordIdToExternalRecordClassMap()
.keySet());
if (!holdsDataManagementPermission) {
enforceRecordWritePermission(recordTypeIdsToDelete, uid);
}
HealthConnectThreadScheduler.schedule(
mContext,
() -> {
try {
mTransactionManager.deleteAll(
new DeleteTransactionRequest(packageName, request, mContext)
.setHasManageHealthDataPermission(
hasDataManagementPermission(uid, pid)));
callback.onResult();
finishDataDeliveryWrite(recordTypeIdsToDelete, uid);
} 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);
}
},
uid,
holdsDataManagementPermission);
}
/** API to get Priority for {@code dataCategory} */
@Override
public void getCurrentPriority(
@NonNull String packageName,
@HealthDataCategory.Type int dataCategory,
@NonNull IGetPriorityResponseCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
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 (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(
callback, exception, HealthConnectException.ERROR_INTERNAL);
}
});
}
/** API to update priority for permission category(ies) */
@Override
public void updatePriority(
@NonNull String packageName,
@NonNull UpdatePriorityRequestParcel updatePriorityRequest,
@NonNull IEmptyResponseCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
HealthDataCategoryPriorityHelper.getInstance()
.setPriorityOrder(
updatePriorityRequest.getDataCategory(),
updatePriorityRequest.getPackagePriorityOrder());
callback.onResult();
} 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);
}
});
}
@Override
public void setRecordRetentionPeriodInDays(
int days, @NonNull UserHandle user, IEmptyResponseCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
AutoDeleteService.setRecordRetentionPeriodInDays(
days, user.getIdentifier());
callback.onResult();
} 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);
}
});
}
@Override
public int getRecordRetentionPeriodInDays(@NonNull UserHandle user) {
try {
mContext.enforceCallingPermission(MANAGE_HEALTH_DATA_PERMISSION, null);
return AutoDeleteService.getRecordRetentionPeriodInDays(user.getIdentifier());
} 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) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
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) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
callback.onResult(
new RecordTypeInfoResponseParcel(
getPopulatedRecordTypeInfoResponses()));
} catch (SQLiteException sqLiteException) {
tryAndThrowException(
callback, sqLiteException, HealthConnectException.ERROR_IO);
} catch (Exception exception) {
tryAndThrowException(
callback, exception, HealthConnectException.ERROR_INTERNAL);
}
});
}
/**
* @see HealthConnectManager#queryAccessLogs
*/
@Override
public void queryAccessLogs(@NonNull String packageName, IAccessLogsResponseCallback callback) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
final List<AccessLog> accessLogsList =
AccessLogsHelper.getInstance().queryAccessLogs();
callback.onResult(new AccessLogsResponseParcel(accessLogsList));
} catch (Exception exception) {
Slog.e(TAG, "Exception: ", exception);
tryAndThrowException(
callback, exception, HealthConnectException.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) {
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
mContext.enforcePermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid, null);
HealthConnectThreadScheduler.scheduleControllerTask(
() -> {
try {
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_SECURITY);
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
tryAndThrowException(callback, e, HealthConnectException.ERROR_INTERNAL);
}
});
}
// TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
// intent.
@Override
public void startMigration(IMigrationCallback callback) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA,
"Caller does not have " + Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
// TODO(b/265000849): Start the migration
callback.onSuccess();
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
// TODO(b/263897830): Send errors properly
tryAndThrowException(callback, e, MigrationException.ERROR_UNKNOWN, null);
}
});
}
// TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
// intent.
@Override
public void finishMigration(IMigrationCallback callback) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA,
"Caller does not have " + Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
// TODO(b/264401271): Finish the migration
callback.onSuccess();
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
// TODO(b/263897830): Send errors properly
tryAndThrowException(callback, e, MigrationException.ERROR_UNKNOWN, null);
}
});
}
// TODO(b/265780725): Update javadocs and ensure that the caller handles SHOW_MIGRATION_INFO
// intent.
@Override
public void writeMigrationData(List<MigrationEntity> entities, IMigrationCallback callback) {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA,
"Caller does not have " + Manifest.permission.MIGRATE_HEALTH_CONNECT_DATA);
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
getDataMigrationManager(getCallingUserHandle()).apply(entities);
callback.onSuccess();
} catch (Exception e) {
Slog.e(TAG, "Exception: ", e);
// TODO(b/263897830): Send errors properly
tryAndThrowException(callback, e, MigrationException.ERROR_UNKNOWN, null);
}
});
}
/**
* @see HealthConnectManager#stageAllHealthConnectRemoteData
*/
@Override
public void stageAllHealthConnectRemoteData(
@NonNull StageRemoteDataRequest stageRemoteDataRequest,
@NonNull UserHandle userHandle,
@NonNull IDataStagingFinishedCallback callback) {
mContext.enforceCallingPermission(
Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA, null);
setDataDownloadState(DATA_DOWNLOAD_COMPLETE, userHandle.getIdentifier(), false /* force */);
@DataRestoreState int curDataRestoreState = getDataRestoreState(userHandle.getIdentifier());
if (curDataRestoreState >= DATA_RESTORE_STAGING_IN_PROGRESS) {
if (curDataRestoreState >= DATA_RESTORE_STAGING_DONE) {
Slog.w(TAG, "Staging is already done. Cur state " + curDataRestoreState);
} else {
// Maybe the caller died and is trying to stage the data again.
Slog.w(TAG, "Already in the process of staging.");
}
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
try {
callback.onResult();
} catch (RemoteException e) {
Log.e(TAG, "Restore response could not be sent to the caller.", e);
}
});
return;
}
setDataRestoreState(
DATA_RESTORE_STAGING_IN_PROGRESS, userHandle.getIdentifier(), false /* force */);
Map<String, ParcelFileDescriptor> origPfdsByFileName =
stageRemoteDataRequest.getPfdsByFileName();
Map<String, HealthConnectException> exceptionsByFileName =
new ArrayMap<>(origPfdsByFileName.size());
Map<String, ParcelFileDescriptor> pfdsByFileName =
new ArrayMap<>(origPfdsByFileName.size());
for (var entry : origPfdsByFileName.entrySet()) {
try {
pfdsByFileName.put(entry.getKey(), entry.getValue().dup());
} catch (IOException e) {
exceptionsByFileName.put(
entry.getKey(),
new HealthConnectException(
HealthConnectException.ERROR_IO, e.getMessage()));
}
}
HealthConnectThreadScheduler.scheduleInternalTask(
() -> {
File stagedRemoteDataDir =
getStagedRemoteDataDirectoryForUser(userHandle.getIdentifier());
try {
stagedRemoteDataDir.mkdirs();
// Now that we have the dir we can try to copy all the data.
// Any exceptions we face will be collected and shared with the caller.
pfdsByFileName.forEach(
(fileName, pfd) -> {
File destination = new File(stagedRemoteDataDir, fileName);
try (FileInputStream inputStream =
new FileInputStream(pfd.getFileDescriptor())) {
Path destinationPath =
FileSystems.getDefault()
.getPath(destination.getAbsolutePath());
Files.copy(
inputStream,
destinationPath,
StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
destination.delete();
exceptionsByFileName.put(
fileName,
new HealthConnectException(
HealthConnectException.ERROR_IO,
e.getMessage()));
} catch (SecurityException e) {
destination.delete();
exceptionsByFileName.put(
fileName,
new HealthConnectException(
HealthConnectException.ERROR_SECURITY,
e.getMessage()));
} finally {
try {
pfd.close();
} catch (IOException e) {
exceptionsByFileName.put(
fileName,
new HealthConnectException(
HealthConnectException.ERROR_IO,
e.getMessage()));
}
}
});
} finally {
// We are done staging all the remote data, update the data restore state.
// Even if we encountered any exception we still say that we are "done" as
// we don't expect the caller to retry and see different results.
setDataRestoreState(
DATA_RESTORE_STAGING_DONE,
userHandle.getIdentifier(),
false /* force */);
// Share the result / exception with the caller.
try {
if (exceptionsByFileName.isEmpty()) {
setWasDataRestoreErrorEncountered(
false, userHandle.getIdentifier());
callback.onResult();
} else {
setWasDataRestoreErrorEncountered(true, userHandle.getIdentifier());
callback.onError(
new StageRemoteDataException(exceptionsByFileName));
}
} catch (RemoteException e) {
Log.e(TAG, "Restore response could not be sent to the caller.", e);
} catch (SecurityException e) {
Log.e(
TAG,
"Restore response could not be sent due to conflicting AIDL "
+ "definitions",
e);
}
}
});
}
/**
* @see HealthConnectManager#deleteAllStagedRemoteData
*/
@Override
public void deleteAllStagedRemoteData(@NonNull UserHandle userHandle) {
mContext.enforceCallingPermission(
DELETE_STAGED_HEALTH_CONNECT_REMOTE_DATA_PERMISSION, null);
deleteDir(getStagedRemoteDataDirectoryForUser(userHandle.getIdentifier()));
setDataDownloadState(
DATA_DOWNLOAD_STATE_UNKNOWN, userHandle.getIdentifier(), true /* force */);
setDataRestoreState(
DATA_RESTORE_STATE_UNKNOWN, userHandle.getIdentifier(), true /* force */);
setWasDataRestoreErrorEncountered(false, userHandle.getIdentifier());
}
/**
* @see HealthConnectManager#updateDataDownloadState
*/
@Override
public void updateDataDownloadState(
@DataDownloadState int downloadState, @NonNull UserHandle userHandle) {
mContext.enforceCallingPermission(
Manifest.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA, null);
setDataDownloadState(downloadState, userHandle.getIdentifier(), false /* force */);
if (downloadState == DATA_DOWNLOAD_COMPLETE) {
setDataRestoreState(
DATA_RESTORE_WAITING_FOR_STAGING,
userHandle.getIdentifier(),
false /* force */);
} else if (downloadState == DATA_DOWNLOAD_FAILED) {
setDataRestoreState(
DATA_RESTORE_MERGING_DONE, userHandle.getIdentifier(), false /* force */);
setWasDataRestoreErrorEncountered(true, userHandle.getIdentifier());
}
}
@VisibleForTesting
Set<String> getStagedRemoteFileNames(int userId) {
return Stream.of(getStagedRemoteDataDirectoryForUser(userId).listFiles())
.filter(file -> !file.isDirectory())
.map(File::getName)
.collect(Collectors.toSet());
}
void setDataRestoreState(@DataRestoreState int dataRestoreState, int userID, boolean force) {
@DataRestoreState int currentRestoreState = getDataRestoreState(userID);
if (!force && currentRestoreState >= dataRestoreState) {
Slog.w(
TAG,
"Attempt to update data restore state in wrong order from "
+ currentRestoreState
+ " to "
+ dataRestoreState);
return;
}
// TODO(b/264070899) Store on a per user basis when we have per user db
PreferenceHelper.getInstance()
.insertPreference(DATA_RESTORE_STATE_KEY, String.valueOf(dataRestoreState));
}
@DataRestoreState
int getDataRestoreState(int userId) {
// TODO(b/264070899) Get on a per user basis when we have per user db
String restoreStateOnDisk =
PreferenceHelper.getInstance().getPreference(DATA_RESTORE_STATE_KEY);
@DataRestoreState int currentRestoreState = DATA_RESTORE_STATE_UNKNOWN;
if (restoreStateOnDisk == null) {
return currentRestoreState;
}
try {
currentRestoreState = Integer.parseInt(restoreStateOnDisk);
} catch (Exception e) {
Slog.e(TAG, "Exception parsing restoreStateOnDisk: " + restoreStateOnDisk, e);
}
return currentRestoreState;
}
@DataDownloadState
private int getDataDownloadState(int userId) {
// TODO(b/264070899) Get on a per user basis when we have per user db
String downloadStateOnDisk =
PreferenceHelper.getInstance().getPreference(DATA_DOWNLOAD_STATE_KEY);
@DataDownloadState int currentDownloadState = DATA_DOWNLOAD_STATE_UNKNOWN;
if (downloadStateOnDisk == null) {
return currentDownloadState;
}
try {
currentDownloadState = Integer.parseInt(downloadStateOnDisk);
} catch (Exception e) {
Slog.e(TAG, "Exception parsing downloadStateOnDisk " + downloadStateOnDisk, e);
}
return currentDownloadState;
}
private void setDataDownloadState(
@DataDownloadState int downloadState, int userId, boolean force) {
@DataDownloadState int currentDownloadState = getDataDownloadState(userId);
if (!force
&& (currentDownloadState == DATA_DOWNLOAD_FAILED
|| currentDownloadState == DATA_DOWNLOAD_COMPLETE)) {
Slog.w(TAG, "HC data download already in terminal state.");
return;
}
// TODO(b/264070899) Store on a per user basis when we have per user db
PreferenceHelper.getInstance()
.insertPreference(DATA_DOWNLOAD_STATE_KEY, String.valueOf(downloadState));
}
// Creating a separate single line method to keep this code close to the rest of the code that
// uses PreferenceHelper to keep data on the disk.
private void setWasDataRestoreErrorEncountered(boolean value, int userId) {
// TODO(b/264070899) Store on a per user basis when we have per user db
PreferenceHelper.getInstance()
.insertPreference(WAS_DATA_RESTORE_ERROR_ENCOUNTERED, Boolean.toString(value));
}
private boolean wasDataRestoreErrorEncountered(int userId) {
// TODO(b/264070899) Get on a per user basis when we have per user db
String wasDataRestoreErrorEncountered =
PreferenceHelper.getInstance().getPreference(WAS_DATA_RESTORE_ERROR_ENCOUNTERED);
return Boolean.toString(true).equalsIgnoreCase(wasDataRestoreErrorEncountered);
}
@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(),
RecordHelperProvider.getInstance());
}
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 void enforceRecordWritePermissionForRecords(
List<RecordInternal<?>> recordInternals, int uid) {
Set<Integer> recordTypeIdsToEnforce = new ArraySet<>();
for (RecordInternal<?> recordInternal : recordInternals) {
recordTypeIdsToEnforce.add(recordInternal.getRecordType());
}
enforceRecordWritePermissionInternal(recordTypeIdsToEnforce.stream().toList(), uid);
}
private boolean hasDataManagementPermission(int uid, int pid) {
return mContext.checkPermission(MANAGE_HEALTH_DATA_PERMISSION, pid, uid)
== PERMISSION_GRANTED;
}
private void enforceRecordWritePermission(List<Integer> recordTypeIds, int uid) {
enforceRecordWritePermissionInternal(recordTypeIds, uid);
}
private void enforceRecordReadPermission(List<Integer> recordTypeIds, int uid) {
for (Integer recordTypeId : recordTypeIds) {
String permissionName =
HealthPermissions.getHealthReadPermission(
RecordTypePermissionCategoryMapper
.getHealthPermissionCategoryForRecordType(recordTypeId));
if (mPermissionManager.checkPermissionForStartDataDelivery(
permissionName, new AttributionSource.Builder(uid).build(), null)
!= PERMISSION_GRANTED) {
throw new SecurityException(
"Caller doesn't have "
+ permissionName
+ " to read record of type "
+ RecordMapper.getInstance()
.getRecordIdToExternalRecordClassMap()
.get(recordTypeId));
}
}
}
/**
* Returns a pair of boolean values. where the first value specifies enforceSelfRead, i.e., the
* app is allowed to read self data, and the second boolean value is true if the caller has
* MANAGE_HEALTH_DATA_PERMISSION, which signifies that the caller is UI.
*/
private boolean enforceRecordReadPermission(int uid, int recordTypeId) {
boolean enforceSelfRead = false;
try {
enforceRecordReadPermission(Collections.singletonList(recordTypeId), uid);
} catch (SecurityException readSecurityException) {
try {
enforceRecordWritePermission(Collections.singletonList(recordTypeId), uid);
// Apps are always allowed to read self data if they have insert
// permission.
enforceSelfRead = true;
} catch (SecurityException writeSecurityException) {
throw readSecurityException;
}
}
return enforceSelfRead;
}
private void enforceRecordWritePermissionInternal(List<Integer> recordTypeIds, int uid) {
for (Integer recordTypeId : recordTypeIds) {
String permissionName =
HealthPermissions.getHealthWritePermission(
RecordTypePermissionCategoryMapper
.getHealthPermissionCategoryForRecordType(recordTypeId));
if (mPermissionManager.checkPermissionForStartDataDelivery(
permissionName, new AttributionSource.Builder(uid).build(), null)
!= PERMISSION_GRANTED) {
throw new SecurityException(
"Caller doesn't have "
+ permissionName
+ " to write to record type "
+ RecordMapper.getInstance()
.getRecordIdToExternalRecordClassMap()
.get(recordTypeId));
}
}
}
private void finishDataDeliveryRead(int recordTypeId, int uid) {
finishDataDeliveryRead(Collections.singletonList(recordTypeId), uid);
}
private void finishDataDeliveryRead(List<Integer> recordTypeIds, int uid) {
try {
for (Integer recordTypeId : recordTypeIds) {
String permissionName =
HealthPermissions.getHealthReadPermission(
RecordTypePermissionCategoryMapper
.getHealthPermissionCategoryForRecordType(recordTypeId));
mPermissionManager.finishDataDelivery(
permissionName, new AttributionSource.Builder(uid).build());
}
} catch (Exception exception) {
// Ignore: HC API has already fulfilled the result, ignore any exception we hit here
}
}
private void finishDataDeliveryWriteRecords(List<RecordInternal<?>> recordInternals, int uid) {
Set<Integer> recordTypeIdsToEnforce = new ArraySet<>();
for (RecordInternal<?> recordInternal : recordInternals) {
recordTypeIdsToEnforce.add(recordInternal.getRecordType());
}
finishDataDeliveryWrite(recordTypeIdsToEnforce.stream().toList(), uid);
}
private void finishDataDeliveryWrite(List<Integer> recordTypeIds, int uid) {
try {
for (Integer recordTypeId : recordTypeIds) {
String permissionName =
HealthPermissions.getHealthWritePermission(
RecordTypePermissionCategoryMapper
.getHealthPermissionCategoryForRecordType(recordTypeId));
mPermissionManager.finishDataDelivery(
permissionName, new AttributionSource.Builder(uid).build());
}
} catch (Exception exception) {
// Ignore: HC API has already fulfilled the result, ignore any exception we hit here
}
}
// TODO(b/264794517) Refactor pure util methods out into a separate class
private static File getDataSystemCeHCDirectoryForUser(int userId) {
// Duplicates the implementation of Environment#getDataSystemCeDirectory
// TODO(b/191059409): Unhide Environment#getDataSystemCeDirectory and switch to it.
File systemCeDir = new File(Environment.getDataDirectory(), "system_ce");
File systemCeUserDir = new File(systemCeDir, String.valueOf(userId));
return new File(systemCeUserDir, "healthconnect");
}
// TODO(b/264794517) Refactor pure util methods out into a separate class
private static void deleteDir(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (var file : files) {
if (file.isDirectory()) {
deleteDir(file);
} else {
file.delete();
}
}
}
dir.delete();
}
// TODO(b/264794517) Refactor pure util methods out into a separate class
private static File getStagedRemoteDataDirectoryForUser(int userId) {
File hcDirectoryForUser = getDataSystemCeHCDirectoryForUser(userId);
return new File(hcDirectoryForUser, "remote_staged");
}
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 IActivityDatesResponseCallback 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 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.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);
}
}
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(errorCode, exception.toString(), failedEntityId));
} catch (RemoteException e) {
Log.e(TAG, "Unable to send result to the callback", e);
}
}
}