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 android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.health.connect.datatypes.RecordTypeIdentifier; 25 import android.health.connect.internal.datatypes.RecordInternal; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.Slog; 29 30 import com.android.server.healthconnect.storage.datatypehelpers.AccessLogsHelper; 31 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper; 32 import com.android.server.healthconnect.storage.datatypehelpers.ChangeLogsHelper; 33 import com.android.server.healthconnect.storage.datatypehelpers.DeviceInfoHelper; 34 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper; 35 import com.android.server.healthconnect.storage.utils.RecordHelperProvider; 36 import com.android.server.healthconnect.storage.utils.StorageUtils; 37 import com.android.server.healthconnect.storage.utils.WhereClauses; 38 39 import java.time.Instant; 40 import java.util.ArrayList; 41 import java.util.Collections; 42 import java.util.List; 43 import java.util.Map; 44 import java.util.Objects; 45 import java.util.Set; 46 import java.util.stream.Collectors; 47 48 /** 49 * Refines a request from what the user sent to a format that makes the most sense for the 50 * TransactionManager. 51 * 52 * <p>Notes, This class refines the request as well by replacing the untrusted fields with the 53 * platform's trusted sources. As a part of that this class populates uuid and package name for all 54 * the entries in {@param records}. 55 * 56 * @hide 57 */ 58 public class UpsertTransactionRequest { 59 private static final String TAG = "HealthConnectUTR"; 60 @NonNull private final List<UpsertTableRequest> mUpsertRequests = new ArrayList<>(); 61 @NonNull private final String mPackageName; 62 private final List<UpsertTableRequest> mAccessLogs = new ArrayList<>(); 63 private final boolean mSkipPackageNameAndLogs; 64 @RecordTypeIdentifier.RecordType Set<Integer> mRecordTypes = new ArraySet<>(); 65 66 private ArrayMap<String, Boolean> mExtraWritePermissionsToState; 67 UpsertTransactionRequest( @ullable String packageName, @NonNull List<RecordInternal<?>> recordInternals, Context context, boolean isInsertRequest, Map<String, Boolean> extraPermsStateMap)68 public UpsertTransactionRequest( 69 @Nullable String packageName, 70 @NonNull List<RecordInternal<?>> recordInternals, 71 Context context, 72 boolean isInsertRequest, 73 Map<String, Boolean> extraPermsStateMap) { 74 this( 75 packageName, 76 recordInternals, 77 context, 78 isInsertRequest, 79 false /* skipPackageNameAndLogs */, 80 extraPermsStateMap); 81 } 82 UpsertTransactionRequest( @ullable String packageName, @NonNull List<RecordInternal<?>> recordInternals, Context context, boolean isInsertRequest, boolean skipPackageNameAndLogs)83 public UpsertTransactionRequest( 84 @Nullable String packageName, 85 @NonNull List<RecordInternal<?>> recordInternals, 86 Context context, 87 boolean isInsertRequest, 88 boolean skipPackageNameAndLogs) { 89 this( 90 packageName, 91 recordInternals, 92 context, 93 isInsertRequest, 94 skipPackageNameAndLogs, 95 Collections.emptyMap()); 96 } 97 UpsertTransactionRequest( @ullable String packageName, @NonNull List<RecordInternal<?>> recordInternals, Context context, boolean isInsertRequest, boolean skipPackageNameAndLogs, Map<String, Boolean> extraPermsStateMap)98 public UpsertTransactionRequest( 99 @Nullable String packageName, 100 @NonNull List<RecordInternal<?>> recordInternals, 101 Context context, 102 boolean isInsertRequest, 103 boolean skipPackageNameAndLogs, 104 Map<String, Boolean> extraPermsStateMap) { 105 mPackageName = packageName; 106 mSkipPackageNameAndLogs = skipPackageNameAndLogs; 107 if (extraPermsStateMap != null && !extraPermsStateMap.isEmpty()) { 108 mExtraWritePermissionsToState = new ArrayMap<>(); 109 mExtraWritePermissionsToState.putAll(extraPermsStateMap); 110 } 111 112 for (RecordInternal<?> recordInternal : recordInternals) { 113 if (!mSkipPackageNameAndLogs) { 114 StorageUtils.addPackageNameTo(recordInternal, packageName); 115 } 116 AppInfoHelper.getInstance() 117 .populateAppInfoId(recordInternal, context, /* requireAllFields= */ true); 118 DeviceInfoHelper.getInstance().populateDeviceInfoId(recordInternal); 119 120 if (isInsertRequest) { 121 // Always generate an uuid field for insert requests, we should not trust what is 122 // already present. 123 StorageUtils.addNameBasedUUIDTo(recordInternal); 124 mRecordTypes.add(recordInternal.getRecordType()); 125 } else { 126 // For update requests, generate uuid if the clientRecordID is present, else use the 127 // uuid passed as input. 128 StorageUtils.updateNameBasedUUIDIfRequired(recordInternal); 129 } 130 recordInternal.setLastModifiedTime(Instant.now().toEpochMilli()); 131 addRequest(recordInternal, isInsertRequest); 132 } 133 134 if (!mRecordTypes.isEmpty()) { 135 if (!mSkipPackageNameAndLogs) { 136 mAccessLogs.add( 137 AccessLogsHelper.getInstance() 138 .getUpsertTableRequest( 139 packageName, new ArrayList<>(mRecordTypes), UPSERT)); 140 } 141 142 Slog.d( 143 TAG, 144 "Upserting transaction for " 145 + packageName 146 + " with size " 147 + recordInternals.size()); 148 } 149 } 150 getAccessLogs()151 public List<UpsertTableRequest> getAccessLogs() { 152 return mAccessLogs; 153 } 154 155 @NonNull getInsertRequestsForChangeLogs()156 public List<UpsertTableRequest> getInsertRequestsForChangeLogs() { 157 if (mSkipPackageNameAndLogs) { 158 return Collections.emptyList(); 159 } 160 long currentTime = Instant.now().toEpochMilli(); 161 ChangeLogsHelper.ChangeLogs insertChangeLogs = 162 new ChangeLogsHelper.ChangeLogs(UPSERT, mPackageName, currentTime); 163 for (UpsertTableRequest upsertRequest : mUpsertRequests) { 164 insertChangeLogs.addUUID( 165 upsertRequest.getRecordInternal().getRecordType(), 166 upsertRequest.getRecordInternal().getAppInfoId(), 167 upsertRequest.getRecordInternal().getUuid()); 168 } 169 170 return insertChangeLogs.getUpsertTableRequests(); 171 } 172 173 @NonNull getUpsertRequests()174 public List<UpsertTableRequest> getUpsertRequests() { 175 return mUpsertRequests; 176 } 177 178 @NonNull getUUIdsInOrder()179 public List<String> getUUIdsInOrder() { 180 return mUpsertRequests.stream() 181 .map((request) -> request.getRecordInternal().getUuid().toString()) 182 .collect(Collectors.toList()); 183 } 184 generateWhereClausesForUpdate(@onNull RecordInternal<?> recordInternal)185 private WhereClauses generateWhereClausesForUpdate(@NonNull RecordInternal<?> recordInternal) { 186 WhereClauses whereClauseForUpdateRequest = new WhereClauses(); 187 whereClauseForUpdateRequest.addWhereEqualsClause( 188 RecordHelper.UUID_COLUMN_NAME, StorageUtils.getHexString(recordInternal.getUuid())); 189 whereClauseForUpdateRequest.addWhereEqualsClause( 190 RecordHelper.APP_INFO_ID_COLUMN_NAME, 191 /* expected args value */ String.valueOf(recordInternal.getAppInfoId())); 192 return whereClauseForUpdateRequest; 193 } 194 addRequest(@onNull RecordInternal<?> recordInternal, boolean isInsertRequest)195 private void addRequest(@NonNull RecordInternal<?> recordInternal, boolean isInsertRequest) { 196 RecordHelper<?> recordHelper = 197 RecordHelperProvider.getInstance().getRecordHelper(recordInternal.getRecordType()); 198 Objects.requireNonNull(recordHelper); 199 200 UpsertTableRequest request = 201 recordHelper.getUpsertTableRequest(recordInternal, mExtraWritePermissionsToState); 202 request.setRecordType(recordHelper.getRecordIdentifier()); 203 if (!isInsertRequest) { 204 request.setUpdateWhereClauses(generateWhereClausesForUpdate(recordInternal)); 205 } 206 request.setRecordInternal(recordInternal); 207 mUpsertRequests.add(request); 208 } 209 } 210