blob: 191ef51f11382125a37716d406407d0ce10be385 [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.healthconnect.storage;
import static android.health.connect.Constants.DEFAULT_LONG;
import static android.health.connect.Constants.DEFAULT_PAGE_SIZE;
import static android.health.connect.Constants.PARENT_KEY;
import static android.health.connect.HealthConnectException.ERROR_INTERNAL;
import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.APP_INFO_ID_COLUMN_NAME;
import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.PRIMARY_COLUMN_NAME;
import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
import android.annotation.NonNull;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.health.connect.Constants;
import android.health.connect.HealthConnectException;
import android.health.connect.internal.datatypes.RecordInternal;
import android.os.UserHandle;
import android.util.Pair;
import android.util.Slog;
import com.android.server.healthconnect.HealthConnectUserContext;
import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper;
import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
import com.android.server.healthconnect.storage.request.AggregateTableRequest;
import com.android.server.healthconnect.storage.request.DeleteTableRequest;
import com.android.server.healthconnect.storage.request.DeleteTransactionRequest;
import com.android.server.healthconnect.storage.request.ReadTableRequest;
import com.android.server.healthconnect.storage.request.ReadTransactionRequest;
import com.android.server.healthconnect.storage.request.UpsertTableRequest;
import com.android.server.healthconnect.storage.request.UpsertTransactionRequest;
import com.android.server.healthconnect.storage.utils.RecordHelperProvider;
import com.android.server.healthconnect.storage.utils.StorageUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
/**
* A class to handle all the DB transaction request from the clients. {@link TransactionManager}
* acts as a layer b/w the DB and the data type helper classes and helps perform actual operations
* on the DB.
*
* @hide
*/
public final class TransactionManager {
private static final String TAG = "HealthConnectTransactionMan";
private static final HashMap<UserHandle, HealthConnectDatabase> mUserHandleToDatabaseMap =
new HashMap<>();
private static TransactionManager sTransactionManager;
private HealthConnectDatabase mHealthConnectDatabase;
private TransactionManager(@NonNull HealthConnectUserContext context) {
mHealthConnectDatabase = new HealthConnectDatabase(context);
mUserHandleToDatabaseMap.put(context.getCurrentUserHandle(), mHealthConnectDatabase);
}
public void onUserUnlocked(@NonNull HealthConnectUserContext healthConnectUserContext) {
if (!mUserHandleToDatabaseMap.containsKey(
healthConnectUserContext.getCurrentUserHandle())) {
mUserHandleToDatabaseMap.put(
healthConnectUserContext.getCurrentUserHandle(),
new HealthConnectDatabase(healthConnectUserContext));
}
mHealthConnectDatabase =
mUserHandleToDatabaseMap.get(healthConnectUserContext.getCurrentUserHandle());
}
/**
* Inserts all the {@link RecordInternal} in {@code request} into the HealthConnect database.
*
* @param request an insert request.
* @return List of uids of the inserted {@link RecordInternal}, in the same order as they
* presented to {@code request}.
*/
public List<String> insertAll(@NonNull UpsertTransactionRequest request)
throws SQLiteException {
if (Constants.DEBUG) {
Slog.d(TAG, "Inserting " + request.getUpsertRequests().size() + " requests.");
}
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
for (UpsertTableRequest upsertRequest : request.getUpsertRequests()) {
insertOrReplaceRecord(db, upsertRequest);
}
for (UpsertTableRequest insertRequestsForChangeLog :
request.getInsertRequestsForChangeLogs()) {
insertRecord(db, insertRequestsForChangeLog);
}
for (UpsertTableRequest insertRequestsForAccessLogs : request.getAccessLogs()) {
insertRecord(db, insertRequestsForAccessLogs);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return request.getUUIdsInOrder();
}
public void insertAll(@NonNull List<UpsertTableRequest> requests) throws SQLiteException {
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
for (UpsertTableRequest request : requests) {
insertOrIgnore(db, request);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Inserts or replaces all the {@link UpsertTableRequest} into the HealthConnect database.
*
* @param upsertTableRequests a list of insert table requests.
*/
public void insertOrReplaceAll(@NonNull List<UpsertTableRequest> upsertTableRequests)
throws SQLiteException {
insertAll(upsertTableRequests, this::insertOrReplaceRecord);
}
/**
* Inserts or ignore on conflicts all the {@link UpsertTableRequest} into the HealthConnect
* database.
*
* @param upsertTableRequests a list of insert table requests.
*/
public void insertOrIgnoreOnConflict(@NonNull List<UpsertTableRequest> upsertTableRequests) {
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
upsertTableRequests.forEach(
(upsertTableRequest) -> insertOrIgnore(db, upsertTableRequest));
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Deletes all the {@link RecordInternal} in {@code request} into the HealthConnect database.
*
* <p>NOTE: Please don't add logic to explicitly delete child table entries here as they should
* be deleted via cascade
*
* @param request a delete request.
*/
public int deleteAll(@NonNull DeleteTransactionRequest request) throws SQLiteException {
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
int numberOfRecordsDeleted = 0;
try {
for (DeleteTableRequest deleteTableRequest : request.getDeleteTableRequests()) {
if (deleteTableRequest.requiresRead()) {
/*
Delete request needs UUID before the entry can be
deleted, fetch and set it in {@code request}
*/
try (Cursor cursor = db.rawQuery(deleteTableRequest.getReadCommand(), null)) {
int numberOfUuidsToDelete = 0;
while (cursor.moveToNext()) {
numberOfUuidsToDelete++;
if (deleteTableRequest.requiresPackageCheck()) {
request.enforcePackageCheck(
StorageUtils.getCursorUUID(
cursor, deleteTableRequest.getIdColumnName()),
StorageUtils.getCursorLong(
cursor, deleteTableRequest.getPackageColumnName()));
}
request.onRecordFetched(
deleteTableRequest.getRecordType(),
StorageUtils.getCursorLong(
cursor, deleteTableRequest.getPackageColumnName()),
StorageUtils.getCursorUUID(
cursor, deleteTableRequest.getIdColumnName()));
}
deleteTableRequest.setNumberOfUuidsToDelete(numberOfUuidsToDelete);
}
}
numberOfRecordsDeleted += deleteTableRequest.getTotalNumberOfRecordsDeleted();
db.execSQL(deleteTableRequest.getDeleteCommand());
}
request.getChangeLogUpsertRequests()
.forEach((insertRequest) -> insertRecord(db, insertRequest));
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
return numberOfRecordsDeleted;
}
/**
* Handles the aggregation requests for {@code aggregateTableRequest}
*
* @param aggregateTableRequest an aggregate request.
*/
@NonNull
public void populateWithAggregation(AggregateTableRequest aggregateTableRequest) {
final SQLiteDatabase db = getReadableDb();
if (!aggregateTableRequest.getRecordHelper().isRecordOperationsEnabled()) {
return;
}
try (Cursor cursor = db.rawQuery(aggregateTableRequest.getAggregationCommand(), null);
Cursor metaDataCursor =
db.rawQuery(
aggregateTableRequest.getCommandToFetchAggregateMetadata(), null)) {
aggregateTableRequest.onResultsFetched(cursor, metaDataCursor);
}
}
/**
* Reads the records {@link RecordInternal} stored in the HealthConnect database.
*
* @param request a read request.
* @return List of records read {@link RecordInternal} from table based on ids.
*/
public List<RecordInternal<?>> readRecords(@NonNull ReadTransactionRequest request)
throws SQLiteException {
List<RecordInternal<?>> recordInternals = new ArrayList<>();
request.getReadRequests()
.forEach(
(readTableRequest -> {
if (readTableRequest.getRecordHelper().isRecordOperationsEnabled()) {
try (Cursor cursor = read(readTableRequest)) {
Objects.requireNonNull(readTableRequest.getRecordHelper());
List<RecordInternal<?>> internalRecords =
readTableRequest
.getRecordHelper()
.getInternalRecords(cursor, DEFAULT_PAGE_SIZE);
populateInternalRecordsWithExtraData(
internalRecords, readTableRequest);
recordInternals.addAll(internalRecords);
}
}
}));
return recordInternals;
}
/**
* Reads the records {@link RecordInternal} stored in the HealthConnect database and returns the
* max row_id as next page token.
*
* @param request a read request.
* @return Pair containing records list read {@link RecordInternal} from the table and a next
* page token for pagination
*/
public Pair<List<RecordInternal<?>>, Long> readRecordsAndGetNextToken(
@NonNull ReadTransactionRequest request) throws SQLiteException {
// throw an exception if read requested is not for a single record type
// i.e. size of read table request is not equal to 1.
if (request.getReadRequests().size() != 1) {
throw new IllegalArgumentException("Read requested is not for a single record type");
}
List<RecordInternal<?>> recordInternalList;
long token = DEFAULT_LONG;
ReadTableRequest readTableRequest = request.getReadRequests().get(0);
RecordHelper<?> helper = readTableRequest.getRecordHelper();
Objects.requireNonNull(helper);
if (!helper.isRecordOperationsEnabled()) {
recordInternalList = new ArrayList<>(0);
return Pair.create(recordInternalList, token);
}
try (Cursor cursor = read(readTableRequest)) {
recordInternalList = helper.getInternalRecords(cursor, readTableRequest.getPageSize());
String startTimeColumnName = helper.getStartTimeColumnName();
populateInternalRecordsWithExtraData(recordInternalList, readTableRequest);
if (cursor.moveToNext()) {
token = getCursorLong(cursor, startTimeColumnName);
}
}
return Pair.create(recordInternalList, token);
}
/**
* Inserts record into the table in {@code request} into the HealthConnect database.
*
* <p>NOTE: PLEASE ONLY USE THIS FUNCTION IF YOU WANT TO INSERT A SINGLE RECORD PER API. PLEASE
* DON'T USE THIS FUNCTION INSIDE A FOR LOOP OR REPEATEDLY: The reason is that this function
* tries to insert a record inside its own transaction and if you are trying to insert multiple
* things using this method in the same api call, they will all get inserted in their separate
* transactions and will be less performant. If at all, the requirement is to insert them in
* different transactions, as they are not related to each, then this method can be used.
*
* @param request an insert request.
* @return rowId of the inserted record.
*/
public long insert(@NonNull UpsertTableRequest request) {
final SQLiteDatabase db = getWritableDb();
return insertRecord(db, request);
}
/**
* Update record into the table in {@code request} into the HealthConnect database.
*
* <p>NOTE: PLEASE ONLY USE THIS FUNCTION IF YOU WANT TO UPDATE A SINGLE RECORD PER API. PLEASE
* DON'T USE THIS FUNCTION INSIDE A FOR LOOP OR REPEATEDLY: The reason is that this function
* tries to update a record inside its own transaction and if you are trying to insert multiple
* things using this method in the same api call, they will all get updates in their separate
* transactions and will be less performant. If at all, the requirement is to update them in
* different transactions, as they are not related to each, then this method can be used.
*
* @param request an update request.
*/
public void update(@NonNull UpsertTableRequest request) {
final SQLiteDatabase db = getWritableDb();
updateRecord(db, request);
}
/**
* Inserts (or updates if the row exists) record into the table in {@code request} into the
* HealthConnect database.
*
* <p>NOTE: PLEASE ONLY USE THIS FUNCTION IF YOU WANT TO UPSERT A SINGLE RECORD. PLEASE DON'T
* USE THIS FUNCTION INSIDE A FOR LOOP OR REPEATEDLY: The reason is that this function tries to
* insert a record out of a transaction and if you are trying to insert a record before or after
* opening up a transaction please rethink if you really want to use this function.
*
* <p>NOTE: INSERT + WITH_CONFLICT_REPLACE only works on unique columns, else in case of
* conflict it leads to abort of the transaction.
*
* @param request an insert request.
* @return rowId of the inserted or updated record.
*/
public long insertOrReplace(@NonNull UpsertTableRequest request) {
final SQLiteDatabase db = getWritableDb();
return insertOrReplaceRecord(db, request);
}
/** Note: It is the responsibility of the caller to close the returned cursor */
@NonNull
public Cursor read(@NonNull ReadTableRequest request) {
if (Constants.DEBUG) {
Slog.d(TAG, "Read query: " + request.getReadCommand());
}
return getReadableDb().rawQuery(request.getReadCommand(), null);
}
public long getLastRowIdFor(String tableName) {
final SQLiteDatabase db = getReadableDb();
try (Cursor cursor = db.rawQuery(StorageUtils.getMaxPrimaryKeyQuery(tableName), null)) {
cursor.moveToFirst();
return cursor.getLong(cursor.getColumnIndex(PRIMARY_COLUMN_NAME));
}
}
/**
* Get number of entries in the given table.
*
* @param tableName Name of table
* @return Number of entries in the given table
*/
public long getNumberOfEntriesInTheTable(@NonNull String tableName) {
Objects.requireNonNull(tableName);
return DatabaseUtils.queryNumEntries(getReadableDb(), tableName);
}
/**
* Size of Health Connect database in bytes.
*
* @param context Context
* @return Size of the database
*/
public long getDatabaseSize(@NonNull Context context) {
Objects.requireNonNull(context);
return context.getDatabasePath(getReadableDb().getPath()).length();
}
public void delete(DeleteTableRequest request) {
final SQLiteDatabase db = getWritableDb();
db.execSQL(request.getDeleteCommand());
}
/**
* Updates all the {@link RecordInternal} in {@code request} into the HealthConnect database.
*
* @param request an update request.
*/
public void updateAll(@NonNull UpsertTransactionRequest request) {
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
for (UpsertTableRequest upsertRequest : request.getUpsertRequests()) {
updateRecord(db, upsertRequest);
}
for (UpsertTableRequest insertRequestsForChangeLog :
request.getInsertRequestsForChangeLogs()) {
insertRecord(db, insertRequestsForChangeLog);
}
for (UpsertTableRequest insertRequestsForAccessLogs : request.getAccessLogs()) {
insertRecord(db, insertRequestsForAccessLogs);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* @return list of distinct packageNames corresponding to the input table name after querying
* the table.
*/
public HashMap<Integer, HashSet<String>> getDistinctPackageNamesForRecordsTable(
Set<Integer> recordTypes) throws SQLiteException {
final SQLiteDatabase db = getReadableDb();
HashMap<Integer, HashSet<String>> packagesForRecordTypeMap = new HashMap<>();
for (Integer recordType : recordTypes) {
RecordHelper<?> recordHelper =
RecordHelperProvider.getInstance().getRecordHelper(recordType);
HashSet<String> packageNamesForDatatype = new HashSet<>();
try (Cursor cursorForDistinctPackageNames =
db.rawQuery(
/* sql query */
recordHelper
.getReadTableRequestWithDistinctAppInfoIds()
.getReadCommand(),
/* selectionArgs */ null)) {
if (cursorForDistinctPackageNames.getCount() > 0) {
AppInfoHelper appInfoHelper = AppInfoHelper.getInstance();
while (cursorForDistinctPackageNames.moveToNext()) {
String packageName =
appInfoHelper.getPackageName(
cursorForDistinctPackageNames.getLong(
cursorForDistinctPackageNames.getColumnIndex(
APP_INFO_ID_COLUMN_NAME)));
if (!packageName.isEmpty()) {
packageNamesForDatatype.add(packageName);
}
}
}
}
packagesForRecordTypeMap.put(recordType, packageNamesForDatatype);
}
return packagesForRecordTypeMap;
}
/**
* ONLY DO OPERATIONS IN A SINGLE TRANSACTION HERE
*
* <p>This is because this function is called from {@link AutoDeleteService}, and we want to
* make sure that either all its operation succeed or fail in a single run.
*/
public void deleteWithoutChangeLogs(@NonNull List<DeleteTableRequest> deleteTableRequests) {
Objects.requireNonNull(deleteTableRequests);
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
for (DeleteTableRequest deleteTableRequest : deleteTableRequests) {
db.execSQL(deleteTableRequest.getDeleteCommand());
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public void onUserSwitching() {
mHealthConnectDatabase.close();
}
private void insertAll(
@NonNull List<UpsertTableRequest> upsertTableRequests,
@NonNull BiConsumer<SQLiteDatabase, UpsertTableRequest> insert) {
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
upsertTableRequests.forEach(
(upsertTableRequest) -> insert.accept(db, upsertTableRequest));
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public <E extends Throwable> void runAsTransaction(TransactionRunnable<E> task) throws E {
final SQLiteDatabase db = getWritableDb();
db.beginTransaction();
try {
task.run(db);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/** Assumes that caller will be closing {@code db} and handling the transaction if required */
public long insertRecord(@NonNull SQLiteDatabase db, @NonNull UpsertTableRequest request) {
long rowId = db.insertOrThrow(request.getTable(), null, request.getContentValues());
request.getChildTableRequests()
.forEach(childRequest -> insertRecord(db, childRequest.withParentKey(rowId)));
return rowId;
}
/**
* Inserts the provided {@link UpsertTableRequest} into the database.
*
* <p>Assumes that caller will be closing {@code db} and handling the transaction if required.
*
* @return the row ID of the newly inserted row or <code>-1</code> if an error occurred.
*/
public long insertOrIgnore(@NonNull SQLiteDatabase db, @NonNull UpsertTableRequest request) {
long rowId =
db.insertWithOnConflict(
request.getTable(),
null,
request.getContentValues(),
SQLiteDatabase.CONFLICT_IGNORE);
if (rowId != -1) {
request.getChildTableRequests()
.forEach(childRequest -> insertRecord(db, childRequest.withParentKey(rowId)));
}
return rowId;
}
/** Note: NEVER close this DB */
@NonNull
private SQLiteDatabase getReadableDb() {
SQLiteDatabase sqLiteDatabase = mHealthConnectDatabase.getReadableDatabase();
if (sqLiteDatabase == null) {
throw new InternalError("SQLite DB not found");
}
return sqLiteDatabase;
}
/** Note: NEVER close this DB */
@NonNull
private SQLiteDatabase getWritableDb() {
SQLiteDatabase sqLiteDatabase = mHealthConnectDatabase.getWritableDatabase();
if (sqLiteDatabase == null) {
throw new InternalError("SQLite DB not found");
}
return sqLiteDatabase;
}
public File getDatabasePath() {
return mHealthConnectDatabase.getDatabasePath();
}
public void updateTable(UpsertTableRequest upsertTableRequest) {
getWritableDb()
.update(
upsertTableRequest.getTable(),
upsertTableRequest.getContentValues(),
upsertTableRequest.getUpdateWhereClauses().get(false),
null);
}
public int getDatabaseVersion() {
return getReadableDb().getVersion();
}
private void updateRecord(SQLiteDatabase db, UpsertTableRequest request) {
// Perform an update operation where UUID and packageName (mapped by appInfoId) is same
// as that of the update request.
long numberOfRowsUpdated =
db.update(
request.getTable(),
request.getContentValues(),
request.getUpdateWhereClauses().get(/* withWhereKeyword */ false),
/* WHERE args */ null);
// throw an exception if the no row was updated, i.e. the uuid with corresponding
// app_id_info for this request is not found in the table.
if (numberOfRowsUpdated == 0) {
throw new IllegalArgumentException(
"No record found for the following input : "
+ new StorageUtils.RecordIdentifierData(request.getContentValues()));
}
if (request.getChildTableRequests().isEmpty()) {
return;
}
try (Cursor cursor =
db.rawQuery(request.getReadRequestUsingUpdateClause().getReadCommand(), null)) {
if (!cursor.moveToFirst()) {
throw new HealthConnectException(
ERROR_INTERNAL, "Expected to read an entry for update, but none found");
}
final long rowId = StorageUtils.getCursorLong(cursor, request.getRowIdColName());
deleteChildTableRequest(request, rowId, db);
insertChildTableRequest(request, rowId, db);
}
}
/**
* Do extra sql requests to populate optional extra data. Used to populate {@link
* android.health.connect.internal.datatypes.ExerciseRouteInternal}.
*/
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());
}
}
/**
* Assumes that caller will be closing {@code db}. Returns -1 in case the update was triggered
* and reading the row_id was not supported on the table.
*
* <p>Note: This function updates rather than the traditional delete + insert in SQLite
*/
private long insertOrReplaceRecord(
@NonNull SQLiteDatabase db, @NonNull UpsertTableRequest request) {
try {
if (request.getUniqueColumnsCount() == 0) {
throw new RuntimeException(
"insertOrReplaceRecord should only be called with unique columns set");
}
long rowId =
db.insertWithOnConflict(
request.getTable(),
null,
request.getContentValues(),
SQLiteDatabase.CONFLICT_FAIL);
insertChildTableRequest(request, rowId, db);
return rowId;
} catch (SQLiteConstraintException e) {
try (Cursor cursor = db.rawQuery(request.getReadRequest().getReadCommand(), null)) {
if (!cursor.moveToFirst()) {
throw new HealthConnectException(
ERROR_INTERNAL, "Conflict found, but couldn't read the entry.");
}
return updateEntriesIfRequired(db, request, cursor);
}
}
}
private long updateEntriesIfRequired(
SQLiteDatabase db, UpsertTableRequest request, Cursor cursor) {
if (!request.requiresUpdate(cursor, request)) {
return -1;
}
db.update(
request.getTable(),
request.getContentValues(),
request.getUpdateWhereClauses().get(/* withWhereKeyword */ false),
/* WHERE args */ null);
if (cursor.getColumnIndex(request.getRowIdColName()) == -1) {
// The table is not explicitly using row_ids hence returning -1 here is ok, as
// the rowid is of no use to this table.
// NOTE: Such tables in HC don't support child tables either as child tables
// inherently require row_ids to have support parent key.
return -1;
}
final long rowId = StorageUtils.getCursorLong(cursor, request.getRowIdColName());
deleteChildTableRequest(request, rowId, db);
insertChildTableRequest(request, rowId, db);
return rowId;
}
private void deleteChildTableRequest(
UpsertTableRequest request, long rowId, SQLiteDatabase db) {
for (String childTable : request.getAllChildTables()) {
DeleteTableRequest deleteTableRequest =
new DeleteTableRequest(childTable).setId(PARENT_KEY, String.valueOf(rowId));
db.execSQL(deleteTableRequest.getDeleteCommand());
}
}
private void insertChildTableRequest(
UpsertTableRequest request, long rowId, SQLiteDatabase db) {
for (UpsertTableRequest childTableRequest : request.getChildTableRequests()) {
db.insertOrThrow(
childTableRequest.withParentKey(rowId).getTable(),
null,
childTableRequest.getContentValues());
}
}
public interface TransactionRunnable<E extends Throwable> {
void run(SQLiteDatabase db) throws E;
}
@NonNull
public static TransactionManager getInstance(@NonNull HealthConnectUserContext context) {
if (sTransactionManager == null) {
sTransactionManager = new TransactionManager(context);
}
return sTransactionManager;
}
@NonNull
public static TransactionManager getInitialisedInstance() {
Objects.requireNonNull(sTransactionManager);
return sTransactionManager;
}
}