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