• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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