1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.healthconnect.storage.request; 18 19 import static android.health.connect.Constants.UPSERT; 20 21 import static com.android.server.healthconnect.storage.utils.StorageUtils.addNameBasedUUIDTo; 22 import static com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.AND; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.Context; 27 import android.health.connect.Constants; 28 import android.health.connect.datatypes.RecordTypeIdentifier; 29 import android.health.connect.internal.datatypes.RecordInternal; 30 import android.util.ArrayMap; 31 import android.util.ArraySet; 32 import android.util.Slog; 33 34 import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper; 35 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper; 36 import com.android.server.healthconnect.storage.datatypehelpers.DeviceInfoHelper; 37 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper; 38 import com.android.server.healthconnect.storage.utils.RecordHelperProvider; 39 import com.android.server.healthconnect.storage.utils.StorageUtils; 40 import com.android.server.healthconnect.storage.utils.WhereClauses; 41 42 import java.time.Instant; 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.Objects; 48 import java.util.Set; 49 import java.util.stream.Collectors; 50 51 /** 52 * Refines a request from what the user sent to a format that makes the most sense for the 53 * TransactionManager. 54 * 55 * <p>Notes, This class refines the request as well by replacing the untrusted fields with the 56 * platform's trusted sources. As a part of that this class populates uuid and package name for all 57 * the entries in {@param records}. 58 * 59 * @hide 60 */ 61 public class UpsertTransactionRequest { 62 private static final String TAG = "HealthConnectUTR"; 63 @NonNull private final List<UpsertTableRequest> mUpsertRequests = new ArrayList<>(); 64 private final List<UpsertTableRequest> mAccessLogs = new ArrayList<>(); 65 private final boolean mSkipPackageNameAndLogs; 66 @RecordTypeIdentifier.RecordType Set<Integer> mRecordTypes = new ArraySet<>(); 67 68 @Nullable private ArrayMap<String, Boolean> mExtraWritePermissionsToState; 69 UpsertTransactionRequest( @ullable String packageName, @NonNull List<RecordInternal<?>> recordInternals, Context context, boolean isInsertRequest, Map<String, Boolean> extraPermsStateMap)70 public UpsertTransactionRequest( 71 @Nullable String packageName, 72 @NonNull List<RecordInternal<?>> recordInternals, 73 Context context, 74 boolean isInsertRequest, 75 Map<String, Boolean> extraPermsStateMap) { 76 this( 77 packageName, 78 recordInternals, 79 context, 80 isInsertRequest, 81 false /* useProvidedUuid */, 82 false /* skipPackageNameAndLogs */, 83 extraPermsStateMap); 84 } 85 UpsertTransactionRequest( @ullable String packageName, @NonNull List<RecordInternal<?>> recordInternals, Context context, boolean isInsertRequest, boolean useProvidedUuid, boolean skipPackageNameAndLogs)86 public UpsertTransactionRequest( 87 @Nullable String packageName, 88 @NonNull List<RecordInternal<?>> recordInternals, 89 Context context, 90 boolean isInsertRequest, 91 boolean useProvidedUuid, 92 boolean skipPackageNameAndLogs) { 93 this( 94 packageName, 95 recordInternals, 96 context, 97 isInsertRequest, 98 useProvidedUuid, 99 skipPackageNameAndLogs, 100 Collections.emptyMap()); 101 } 102 103 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression UpsertTransactionRequest( @ullable String packageName, @NonNull List<RecordInternal<?>> recordInternals, Context context, boolean isInsertRequest, boolean useProvidedUuid, boolean skipPackageNameAndLogs, Map<String, Boolean> extraPermsStateMap)104 private UpsertTransactionRequest( 105 @Nullable String packageName, 106 @NonNull List<RecordInternal<?>> recordInternals, 107 Context context, 108 boolean isInsertRequest, 109 // TODO(b/329237732): Use builder pattern for this class. 110 boolean useProvidedUuid, 111 boolean skipPackageNameAndLogs, 112 Map<String, Boolean> extraPermsStateMap) { 113 mSkipPackageNameAndLogs = skipPackageNameAndLogs; 114 if (extraPermsStateMap != null && !extraPermsStateMap.isEmpty()) { 115 mExtraWritePermissionsToState = new ArrayMap<>(); 116 mExtraWritePermissionsToState.putAll(extraPermsStateMap); 117 } 118 119 for (RecordInternal<?> recordInternal : recordInternals) { 120 if (!mSkipPackageNameAndLogs) { 121 StorageUtils.addPackageNameTo(recordInternal, packageName); 122 } 123 AppInfoHelper.getInstance() 124 .populateAppInfoId(recordInternal, context, /* requireAllFields= */ true); 125 DeviceInfoHelper.getInstance().populateDeviceInfoId(recordInternal); 126 127 if (isInsertRequest) { 128 if (useProvidedUuid && recordInternal.getUuid() != null) { 129 // Do nothing i.e. leave the UUID as provided. This is desired for backup and 130 // restore to ensure references between records remain intact. 131 } else { 132 // Otherwise, we should generate a fresh UUID. Don't let the client choose it. 133 addNameBasedUUIDTo(recordInternal); 134 } 135 } else { 136 // For update requests, generate uuid if the clientRecordID is present, else use the 137 // uuid passed as input. 138 StorageUtils.updateNameBasedUUIDIfRequired(recordInternal); 139 } 140 mRecordTypes.add(recordInternal.getRecordType()); 141 recordInternal.setLastModifiedTime(Instant.now().toEpochMilli()); 142 addRequest(recordInternal, isInsertRequest); 143 } 144 145 if (!mRecordTypes.isEmpty()) { 146 if (!mSkipPackageNameAndLogs) { 147 mAccessLogs.add( 148 AccessLogsHelper.getUpsertTableRequest( 149 packageName, new ArrayList<>(mRecordTypes), UPSERT)); 150 } 151 152 if (Constants.DEBUG) { 153 Slog.d( 154 TAG, 155 "Upserting transaction for " 156 + packageName 157 + " with size " 158 + recordInternals.size()); 159 } 160 } 161 } 162 getAccessLogs()163 public List<UpsertTableRequest> getAccessLogs() { 164 return mAccessLogs; 165 } 166 167 @NonNull getUpsertRequests()168 public List<UpsertTableRequest> getUpsertRequests() { 169 return mUpsertRequests; 170 } 171 172 @NonNull getUUIdsInOrder()173 public List<String> getUUIdsInOrder() { 174 return mUpsertRequests.stream() 175 .map((request) -> request.getRecordInternal().getUuid().toString()) 176 .collect(Collectors.toList()); 177 } 178 generateWhereClausesForUpdate(@onNull RecordInternal<?> recordInternal)179 private WhereClauses generateWhereClausesForUpdate(@NonNull RecordInternal<?> recordInternal) { 180 WhereClauses whereClauseForUpdateRequest = new WhereClauses(AND); 181 whereClauseForUpdateRequest.addWhereEqualsClause( 182 RecordHelper.UUID_COLUMN_NAME, StorageUtils.getHexString(recordInternal.getUuid())); 183 whereClauseForUpdateRequest.addWhereEqualsClause( 184 RecordHelper.APP_INFO_ID_COLUMN_NAME, 185 /* expected args value */ String.valueOf(recordInternal.getAppInfoId())); 186 return whereClauseForUpdateRequest; 187 } 188 addRequest(@onNull RecordInternal<?> recordInternal, boolean isInsertRequest)189 private void addRequest(@NonNull RecordInternal<?> recordInternal, boolean isInsertRequest) { 190 RecordHelper<?> recordHelper = 191 RecordHelperProvider.getRecordHelper(recordInternal.getRecordType()); 192 Objects.requireNonNull(recordHelper); 193 194 UpsertTableRequest request = 195 recordHelper.getUpsertTableRequest(recordInternal, mExtraWritePermissionsToState); 196 request.setRecordType(recordHelper.getRecordIdentifier()); 197 if (!isInsertRequest) { 198 request.setUpdateWhereClauses(generateWhereClausesForUpdate(recordInternal)); 199 } 200 request.setRecordInternal(recordInternal); 201 mUpsertRequests.add(request); 202 } 203 } 204