blob: a101d121c656739bad4e688595baece1b6598216 [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.backuprestore;
import static android.health.connect.Constants.DEFAULT_LONG;
import static android.health.connect.HealthConnectDataState.RESTORE_ERROR_FETCHING_DATA;
import static android.health.connect.HealthConnectDataState.RESTORE_ERROR_NONE;
import static android.health.connect.HealthConnectDataState.RESTORE_ERROR_VERSION_DIFF;
import static android.health.connect.HealthConnectDataState.RESTORE_STATE_IDLE;
import static android.health.connect.HealthConnectDataState.RESTORE_STATE_IN_PROGRESS;
import static android.health.connect.HealthConnectDataState.RESTORE_STATE_PENDING;
import static android.health.connect.HealthConnectManager.DATA_DOWNLOAD_COMPLETE;
import static android.health.connect.HealthConnectManager.DATA_DOWNLOAD_FAILED;
import static android.health.connect.HealthConnectManager.DATA_DOWNLOAD_STATE_UNKNOWN;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorBlob;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.content.Context;
import android.content.ContextWrapper;
import android.database.Cursor;
import android.health.connect.HealthConnectDataState;
import android.health.connect.HealthConnectException;
import android.health.connect.HealthConnectManager.DataDownloadState;
import android.health.connect.ReadRecordsRequestUsingFilters;
import android.health.connect.aidl.IDataStagingFinishedCallback;
import android.health.connect.datatypes.Record;
import android.health.connect.internal.datatypes.RecordInternal;
import android.health.connect.internal.datatypes.utils.RecordMapper;
import android.health.connect.restore.BackupFileNamesSet;
import android.health.connect.restore.StageRemoteDataException;
import android.health.connect.restore.StageRemoteDataRequest;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.healthconnect.permission.FirstGrantTimeManager;
import com.android.server.healthconnect.storage.HealthConnectDatabase;
import com.android.server.healthconnect.storage.TransactionManager;
import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
import com.android.server.healthconnect.storage.request.DeleteTableRequest;
import com.android.server.healthconnect.storage.request.ReadTableRequest;
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 com.android.server.healthconnect.utils.FilesUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
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.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Class that takes up the responsibility to perform backup / restore related tasks.
*
* @hide
*/
public final class BackupRestore {
// Key for storing the current data download state
@VisibleForTesting
public static final String DATA_DOWNLOAD_STATE_KEY = "data_download_state_key";
// The below values for the IntDef are defined in chronological order of the restore process.
public static final int INTERNAL_RESTORE_STATE_UNKNOWN = 0;
public static final int INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING = 1;
public static final int INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS = 2;
public static final int INTERNAL_RESTORE_STATE_STAGING_DONE = 3;
public static final int INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS = 4;
public static final int INTERNAL_RESTORE_STATE_MERGING_DONE = 5;
// Key for storing the current data restore state on disk.
public static final String DATA_RESTORE_STATE_KEY = "data_restore_state_key";
// Key for storing the error restoring HC data.
public static final String DATA_RESTORE_ERROR_KEY = "data_restore_error_key";
private static final String TAG = "HealthConnectBackupRestore";
private final ReentrantReadWriteLock mStatesLock = new ReentrantReadWriteLock(true);
private final FirstGrantTimeManager mFirstGrantTimeManager;
private final Context mStagedDbContext;
private final Context mContext;
private final Map<Long, String> mStagedPackageNamesByAppIds = new ArrayMap<>();
private final Object mMergingLock = new Object();
@GuardedBy("mMergingLock")
private HealthConnectDatabase mStagedDatabase;
private boolean mActivelyStagingRemoteData = false;
public BackupRestore(FirstGrantTimeManager firstGrantTimeManager, @NonNull Context context) {
mFirstGrantTimeManager = firstGrantTimeManager;
mStagedDbContext = new StagedDatabaseContext(context);
mContext = context;
}
/**
* Prepares for staging all health connect remote data.
*
* @return true if the preparation was successful. false either if staging already in progress
* or done.
*/
public boolean prepForStagingIfNotAlreadyDone(int userId) {
mStatesLock.writeLock().lock();
try {
setDataDownloadState(DATA_DOWNLOAD_COMPLETE, userId, false /* force */);
@InternalRestoreState int curDataRestoreState = getInternalRestoreState(userId);
if (curDataRestoreState >= INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS) {
if (curDataRestoreState >= INTERNAL_RESTORE_STATE_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.");
}
return false;
}
mActivelyStagingRemoteData = true;
setInternalRestoreState(
INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS, userId, false /* force */);
return true;
} finally {
mStatesLock.writeLock().unlock();
}
}
/**
* Stages all health connect remote data for merging later.
*
* <p>This should be called on the proper thread.
*/
public void stageAllHealthConnectRemoteData(
Map<String, ParcelFileDescriptor> pfdsByFileName,
Map<String, HealthConnectException> exceptionsByFileName,
int userId,
@NonNull IDataStagingFinishedCallback callback) {
File stagedRemoteDataDir = getStagedRemoteDataDirectoryForUser(userId);
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.
setInternalRestoreState(INTERNAL_RESTORE_STATE_STAGING_DONE, userId, false);
mActivelyStagingRemoteData = false;
// Share the result / exception with the caller.
try {
if (exceptionsByFileName.isEmpty()) {
callback.onResult();
} else {
setDataRestoreError(RESTORE_ERROR_FETCHING_DATA, userId);
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);
} finally {
// Now that the callback for the stageAllHealthConnectRemoteData API has been called
// we can start the merging process.
merge(userId);
}
}
}
/** Writes the backup data into files represented by the passed file descriptors. */
public void getAllDataForBackup(
@NonNull StageRemoteDataRequest stageRemoteDataRequest,
@NonNull UserHandle userHandle) {
Map<String, ParcelFileDescriptor> pfdsByFileName =
stageRemoteDataRequest.getPfdsByFileName();
pfdsByFileName.forEach(
(fileName, pfd) -> {
var backupFilesByFileNames = getBackupFilesByFileNames(userHandle, false);
Path sourceFilePath = backupFilesByFileNames.get(fileName).toPath();
try (FileOutputStream outputStream =
new FileOutputStream(pfd.getFileDescriptor())) {
Files.copy(sourceFilePath, outputStream);
} catch (IOException | SecurityException e) {
Slog.e(TAG, "Failed to send " + fileName + " for backup", e);
} finally {
try {
pfd.close();
} catch (IOException e) {
Slog.e(TAG, "Failed to close " + fileName + " for backup", e);
}
}
});
}
/** Get the file names of all the files that are transported during backup / restore. */
public BackupFileNamesSet getAllBackupFileNames(
@NonNull UserHandle userHandle, boolean forDeviceToDevice) {
return new BackupFileNamesSet(
getBackupFilesByFileNames(userHandle, !forDeviceToDevice).keySet());
}
private Map<String, File> getBackupFilesByFileNames(
UserHandle userHandle, boolean excludeLargeFiles) {
ArrayMap<String, File> backupFilesByFileNames = new ArrayMap<>();
if (!excludeLargeFiles) {
File databasePath = TransactionManager.getInitialisedInstance().getDatabasePath();
backupFilesByFileNames.put(databasePath.getName(), databasePath);
}
File grantTimeFile = mFirstGrantTimeManager.getFile(userHandle);
backupFilesByFileNames.put(grantTimeFile.getName(), grantTimeFile);
return backupFilesByFileNames;
}
/** Updates the download state of the remote data. */
public void updateDataDownloadState(
@DataDownloadState int downloadState, @NonNull UserHandle userHandle) {
setDataDownloadState(downloadState, userHandle.getIdentifier(), false /* force */);
if (downloadState == DATA_DOWNLOAD_COMPLETE) {
setInternalRestoreState(
INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING,
userHandle.getIdentifier(),
false /* force */);
} else if (downloadState == DATA_DOWNLOAD_FAILED) {
setInternalRestoreState(
INTERNAL_RESTORE_STATE_MERGING_DONE,
userHandle.getIdentifier(),
false /* force */);
setDataRestoreError(RESTORE_ERROR_FETCHING_DATA, userHandle.getIdentifier());
}
}
/** Deletes all the staged data and resets all the states. */
public void deleteAndResetEverything(@NonNull UserHandle userHandle) {
// Don't delete anything while we are in the process of merging staged data.
synchronized (mMergingLock) {
mStagedDbContext.deleteDatabase(HealthConnectDatabase.getName());
mStagedDatabase = null;
FilesUtil.deleteDir(getStagedRemoteDataDirectoryForUser(userHandle.getIdentifier()));
}
setDataDownloadState(
DATA_DOWNLOAD_STATE_UNKNOWN, userHandle.getIdentifier(), true /* force */);
setInternalRestoreState(
INTERNAL_RESTORE_STATE_UNKNOWN, userHandle.getIdentifier(), true /* force */);
setDataRestoreError(RESTORE_ERROR_NONE, userHandle.getIdentifier());
}
/** Shares the {@link HealthConnectDataState} in the provided callback. */
public @HealthConnectDataState.DataRestoreState int getDataRestoreState(int userId) {
@HealthConnectDataState.DataRestoreState int dataRestoreState = RESTORE_STATE_IDLE;
@InternalRestoreState int currentRestoreState = getInternalRestoreState(userId);
if (currentRestoreState == INTERNAL_RESTORE_STATE_MERGING_DONE) {
// already with correct values.
} else if (currentRestoreState == INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS) {
dataRestoreState = RESTORE_STATE_IN_PROGRESS;
} else if (currentRestoreState != INTERNAL_RESTORE_STATE_UNKNOWN) {
dataRestoreState = RESTORE_STATE_PENDING;
}
@DataDownloadState int currentDownloadState = getDataDownloadState(userId);
if (currentDownloadState == DATA_DOWNLOAD_FAILED) {
// already with correct values.
} else if (currentDownloadState != DATA_DOWNLOAD_STATE_UNKNOWN) {
dataRestoreState = RESTORE_STATE_PENDING;
}
return dataRestoreState;
}
/** Get the current data restore error. */
public @HealthConnectDataState.DataRestoreError int getDataRestoreError(int userId) {
// TODO(b/264070899) Get on a per user basis when we have per user db
@HealthConnectDataState.DataRestoreError int dataRestoreError = RESTORE_ERROR_NONE;
String restoreErrorOnDisk =
PreferenceHelper.getInstance().getPreference(DATA_RESTORE_ERROR_KEY);
try {
dataRestoreError = Integer.parseInt(restoreErrorOnDisk);
} catch (Exception e) {
Slog.e(TAG, "Exception parsing restoreErrorOnDisk " + restoreErrorOnDisk, e);
}
return dataRestoreError;
}
/** Returns the file names of all the staged files. */
@VisibleForTesting
public Set<String> getStagedRemoteFileNames(int userId) {
return Stream.of(getStagedRemoteDataDirectoryForUser(userId).listFiles())
.filter(file -> !file.isDirectory())
.map(File::getName)
.collect(Collectors.toSet());
}
/** Returns true if restore merging is in progress. API calls are blocked when this is true. */
public boolean isRestoreMergingInProgress(int userId) {
return getInternalRestoreState(userId) == INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS;
}
void setInternalRestoreState(
@InternalRestoreState int dataRestoreState, int userID, boolean force) {
@InternalRestoreState int currentRestoreState = getInternalRestoreState(userID);
mStatesLock.writeLock().lock();
try {
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()
.insertOrReplacePreference(
DATA_RESTORE_STATE_KEY, String.valueOf(dataRestoreState));
} finally {
mStatesLock.writeLock().unlock();
}
}
@InternalRestoreState
int getInternalRestoreState(int userId) {
mStatesLock.readLock().lock();
try {
// TODO(b/264070899) Get on a per user basis when we have per user db
String restoreStateOnDisk =
PreferenceHelper.getInstance().getPreference(DATA_RESTORE_STATE_KEY);
@InternalRestoreState int currentRestoreState = INTERNAL_RESTORE_STATE_UNKNOWN;
if (restoreStateOnDisk == null) {
return currentRestoreState;
}
try {
currentRestoreState = Integer.parseInt(restoreStateOnDisk);
} catch (Exception e) {
Slog.e(TAG, "Exception parsing restoreStateOnDisk: " + restoreStateOnDisk, e);
}
// If we are not actively staging the data right now but the disk still reflects that we
// are then that means we died in the middle of staging. We should be waiting for the
// remote data to be staged now.
if (!mActivelyStagingRemoteData
&& currentRestoreState == INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS) {
currentRestoreState = INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING;
}
return currentRestoreState;
} finally {
mStatesLock.readLock().unlock();
}
}
@DataDownloadState
private int getDataDownloadState(int userId) {
mStatesLock.readLock().lock();
try {
// 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;
} finally {
mStatesLock.readLock().unlock();
}
}
private void setDataDownloadState(
@DataDownloadState int downloadState, int userId, boolean force) {
mStatesLock.writeLock().lock();
try {
@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()
.insertOrReplacePreference(
DATA_DOWNLOAD_STATE_KEY, String.valueOf(downloadState));
} finally {
mStatesLock.writeLock().unlock();
}
}
// 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 setDataRestoreError(
@HealthConnectDataState.DataRestoreError int dataRestoreError, int userId) {
// TODO(b/264070899) Store on a per user basis when we have per user db
PreferenceHelper.getInstance()
.insertOrReplacePreference(
DATA_RESTORE_ERROR_KEY, String.valueOf(dataRestoreError));
}
private void merge(int userId) {
if (getInternalRestoreState(userId) >= INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS) {
return;
}
// TODO(b/266398937): check if data sync in progress once available.
mergeDatabase(userId);
setInternalRestoreState(INTERNAL_RESTORE_STATE_MERGING_DONE, userId, false);
}
private void mergeDatabase(int userId) {
synchronized (mMergingLock) {
if (!mStagedDbContext.getDatabasePath(HealthConnectDatabase.getName()).exists()) {
// no db was staged
return;
}
int currentDbVersion = TransactionManager.getInitialisedInstance().getDatabaseVersion();
int stagedDbVersion = getStagedDatabase().getReadableDatabase().getVersion();
if (currentDbVersion < stagedDbVersion) {
setDataRestoreError(RESTORE_ERROR_VERSION_DIFF, userId);
return;
}
// We never read from the staged db if the module version is behind the staged db
// version. So, we are guaranteed that the merging code will be able to read all the
// records from the db - as the upcoming code is guaranteed to understand the records
// present in the staged db.
// We are sure to migrate the db now, so prepare
prepInternalDataPerStagedDb();
// Go through each record type and migrate all records of that type.
var recordTypeMap = RecordMapper.getInstance().getRecordIdToExternalRecordClassMap();
for (var recordTypeMapEntry : recordTypeMap.entrySet()) {
mergeRecordsOfType(recordTypeMapEntry.getKey(), recordTypeMapEntry.getValue());
}
}
}
private <T extends Record> void mergeRecordsOfType(int recordType, Class<T> recordTypeClass) {
RecordHelper<?> recordHelper =
RecordHelperProvider.getInstance().getRecordHelper(recordType);
// Read all the records of the given type from the staged db and insert them into the
// existing healthconnect db.
long token = DEFAULT_LONG;
do {
var recordsToMergeAndToken = getRecordsToMerge(recordTypeClass, token, recordHelper);
if (recordsToMergeAndToken.first.isEmpty()) {
break;
}
// Using null package name for making insertion for two reasons:
// 1. we don't want to update the logs for this package.
// 2. we don't want to update the package name in the records as they already have the
// correct package name.
UpsertTransactionRequest upsertTransactionRequest =
new UpsertTransactionRequest(
null /* packageName */,
recordsToMergeAndToken.first,
mContext,
true /* isInsertRequest */,
true /* skipPackageNameAndLogs */);
TransactionManager.getInitialisedInstance().insertAll(upsertTransactionRequest);
token = DEFAULT_LONG;
if (recordsToMergeAndToken.second != DEFAULT_LONG) {
token = recordsToMergeAndToken.second * 2;
}
} while (token != DEFAULT_LONG);
// Once all the records of this type have been merged we can delete the table.
// Passing -1 for startTime and endTime as we don't want to have time based filtering in the
// final query.
DeleteTableRequest deleteTableRequest =
recordHelper.getDeleteTableRequest(
null, DEFAULT_LONG /* startTime */, DEFAULT_LONG /* endTime */);
getStagedDatabase().getWritableDatabase().execSQL(deleteTableRequest.getDeleteCommand());
}
private <T extends Record> Pair<List<RecordInternal<?>>, Long> getRecordsToMerge(
Class<T> recordTypeClass, long requestToken, RecordHelper<?> recordHelper) {
ReadRecordsRequestUsingFilters<T> readRecordsRequest =
new ReadRecordsRequestUsingFilters.Builder<>(recordTypeClass)
.setAscending(true)
.setPageSize(2000)
.setPageToken(requestToken)
.build();
Map<String, Boolean> extraReadPermsMapping = new ArrayMap<>();
List<String> extraReadPerms = recordHelper.getExtraReadPermissions();
for (var extraReadPerm : extraReadPerms) {
extraReadPermsMapping.put(extraReadPerm, true);
}
// Working with startDateAccess of -1 as we don't want to have time based filtering in the
// query.
ReadTransactionRequest readTransactionRequest =
new ReadTransactionRequest(
null,
readRecordsRequest.toReadRecordsRequestParcel(),
DEFAULT_LONG /* startDateAccess */,
false,
extraReadPermsMapping);
List<RecordInternal<?>> recordInternalList;
long token = DEFAULT_LONG;
ReadTableRequest readTableRequest = readTransactionRequest.getReadRequests().get(0);
try (Cursor cursor = read(readTableRequest)) {
recordInternalList =
recordHelper.getInternalRecords(
cursor, readTableRequest.getPageSize(), mStagedPackageNamesByAppIds);
String startTimeColumnName = recordHelper.getStartTimeColumnName();
populateInternalRecordsWithExtraData(recordInternalList, readTableRequest);
// Get the token for the next read request.
if (cursor.moveToNext()) {
token = getCursorLong(cursor, startTimeColumnName);
}
}
return Pair.create(recordInternalList, token);
}
private Cursor read(ReadTableRequest request) {
synchronized (mMergingLock) {
return mStagedDatabase.getReadableDatabase().rawQuery(request.getReadCommand(), null);
}
}
private void populateInternalRecordsWithExtraData(
List<RecordInternal<?>> records, ReadTableRequest request) {
if (request.getExtraReadRequests() == null) {
return;
}
for (ReadTableRequest extraDataRequest : request.getExtraReadRequests()) {
Cursor cursorExtraData = read(extraDataRequest);
request.getRecordHelper()
.updateInternalRecordsWithExtraFields(
records, cursorExtraData, extraDataRequest.getTableName());
}
}
private void prepInternalDataPerStagedDb() {
try (Cursor cursor = read(new ReadTableRequest(AppInfoHelper.TABLE_NAME))) {
while (cursor.moveToNext()) {
long rowId = getCursorLong(cursor, RecordHelper.PRIMARY_COLUMN_NAME);
String packageName = getCursorString(cursor, AppInfoHelper.PACKAGE_COLUMN_NAME);
String appName = getCursorString(cursor, AppInfoHelper.APPLICATION_COLUMN_NAME);
byte[] icon = getCursorBlob(cursor, AppInfoHelper.APP_ICON_COLUMN_NAME);
mStagedPackageNamesByAppIds.put(rowId, packageName);
// If this package is not installed on the target device and is not present in the
// health db, then fill the health db with the info from source db.
AppInfoHelper.getInstance()
.addOrUpdateAppInfoIfNotInstalled(
mContext, packageName, appName, icon, false /* onlyReplace */);
}
}
}
private HealthConnectDatabase getStagedDatabase() {
synchronized (mMergingLock) {
if (mStagedDatabase == null) {
mStagedDatabase = new HealthConnectDatabase(mStagedDbContext);
}
return mStagedDatabase;
}
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({
INTERNAL_RESTORE_STATE_UNKNOWN,
INTERNAL_RESTORE_STATE_WAITING_FOR_STAGING,
INTERNAL_RESTORE_STATE_STAGING_IN_PROGRESS,
INTERNAL_RESTORE_STATE_STAGING_DONE,
INTERNAL_RESTORE_STATE_MERGING_IN_PROGRESS,
INTERNAL_RESTORE_STATE_MERGING_DONE
})
public @interface InternalRestoreState {}
/**
* Get the dir for the user with all the staged data - either from the cloud restore or from the
* d2d process.
*/
private static File getStagedRemoteDataDirectoryForUser(int userId) {
File hcDirectoryForUser = FilesUtil.getDataSystemCeHCDirectoryForUser(userId);
return new File(hcDirectoryForUser, "remote_staged");
}
/**
* {@link Context} for the staged health connect db.
*
* @hide
*/
private static final class StagedDatabaseContext extends ContextWrapper {
StagedDatabaseContext(@NonNull Context context) {
super(context);
Objects.requireNonNull(context);
}
@Override
public File getDatabasePath(String name) {
File stagedDataDir = getStagedRemoteDataDirectoryForUser(0);
stagedDataDir.mkdirs();
return new File(stagedDataDir, name);
}
}
}