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 com.android.server.healthconnect.storage.utils.WhereClauses.LogicalOperator.OR;
20 
21 import android.annotation.IntDef;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.content.ContentValues;
25 import android.database.Cursor;
26 import android.health.connect.datatypes.RecordTypeIdentifier;
27 import android.health.connect.internal.datatypes.RecordInternal;
28 import android.util.ArrayMap;
29 import android.util.Pair;
30 
31 import com.android.server.healthconnect.storage.datatypehelpers.RecordHelper;
32 import com.android.server.healthconnect.storage.utils.StorageUtils;
33 import com.android.server.healthconnect.storage.utils.WhereClauses;
34 
35 import java.lang.annotation.ElementType;
36 import java.lang.annotation.Retention;
37 import java.lang.annotation.RetentionPolicy;
38 import java.lang.annotation.Target;
39 import java.util.Collections;
40 import java.util.List;
41 import java.util.Objects;
42 
43 /** @hide */
44 public class UpsertTableRequest {
45     public static final int INVALID_ROW_ID = -1;
46 
47     public static final int TYPE_STRING = 0;
48     public static final int TYPE_BLOB = 1;
49     private final String mTable;
50     private ContentValues mContentValues;
51     private final List<Pair<String, Integer>> mUniqueColumns;
52     private List<UpsertTableRequest> mChildTableRequests = Collections.emptyList();
53     private String mParentCol;
54     private long mRowId = INVALID_ROW_ID;
55     private WhereClauses mWhereClausesForUpdate;
56     private IRequiresUpdate mRequiresUpdate = new IRequiresUpdate() {};
57     private Integer mRecordType;
58     private RecordInternal<?> mRecordInternal;
59     private RecordHelper<?> mRecordHelper;
60     private List<String> mPostUpsertCommands = Collections.emptyList();
61 
62     @Nullable private ArrayMap<String, Boolean> mExtraWritePermissionsStateMapping;
63 
UpsertTableRequest(@onNull String table, @NonNull ContentValues contentValues)64     public UpsertTableRequest(@NonNull String table, @NonNull ContentValues contentValues) {
65         this(table, contentValues, Collections.emptyList());
66     }
67 
68     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
UpsertTableRequest( @onNull String table, @NonNull ContentValues contentValues, @NonNull List<Pair<String, Integer>> uniqueColumns)69     public UpsertTableRequest(
70             @NonNull String table,
71             @NonNull ContentValues contentValues,
72             @NonNull List<Pair<String, Integer>> uniqueColumns) {
73         Objects.requireNonNull(table);
74         Objects.requireNonNull(contentValues);
75         Objects.requireNonNull(uniqueColumns);
76 
77         mTable = table;
78         mContentValues = contentValues;
79         mUniqueColumns = uniqueColumns;
80     }
81 
getUniqueColumnsCount()82     public int getUniqueColumnsCount() {
83         return mUniqueColumns.size();
84     }
85 
86     @NonNull
withParentKey(long rowId)87     public UpsertTableRequest withParentKey(long rowId) {
88         mRowId = rowId;
89         return this;
90     }
91 
92     /**
93      * Use this if you want to add row_id of the parent table to all the child entries in {@code
94      * parentCol}
95      */
96     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
97     @NonNull
setParentColumnForChildTables(@ullable String parentCol)98     public UpsertTableRequest setParentColumnForChildTables(@Nullable String parentCol) {
99         mParentCol = parentCol;
100         return this;
101     }
102 
103     @NonNull
setRequiresUpdateClause(@onNull IRequiresUpdate requiresUpdate)104     public UpsertTableRequest setRequiresUpdateClause(@NonNull IRequiresUpdate requiresUpdate) {
105         Objects.requireNonNull(requiresUpdate);
106 
107         mRequiresUpdate = requiresUpdate;
108         return this;
109     }
110 
111     @NonNull
getTable()112     public String getTable() {
113         return mTable;
114     }
115 
116     @NonNull
getContentValues()117     public ContentValues getContentValues() {
118         // Set the parent column of the creator of this requested to do that
119         if (!Objects.isNull(mParentCol) && mRowId != INVALID_ROW_ID) {
120             mContentValues.put(mParentCol, mRowId);
121         }
122 
123         return mContentValues;
124     }
125 
126     @NonNull
getChildTableRequests()127     public List<UpsertTableRequest> getChildTableRequests() {
128         return mChildTableRequests;
129     }
130 
131     @NonNull
setChildTableRequests( @onNull List<UpsertTableRequest> childTableRequests)132     public UpsertTableRequest setChildTableRequests(
133             @NonNull List<UpsertTableRequest> childTableRequests) {
134         Objects.requireNonNull(childTableRequests);
135 
136         mChildTableRequests = childTableRequests;
137         return this;
138     }
139 
140     @NonNull
getUpdateWhereClauses()141     public WhereClauses getUpdateWhereClauses() {
142         if (mWhereClausesForUpdate == null) {
143             return getReadWhereClauses();
144         }
145 
146         return mWhereClausesForUpdate;
147     }
148 
setUpdateWhereClauses(WhereClauses whereClauses)149     public UpsertTableRequest setUpdateWhereClauses(WhereClauses whereClauses) {
150         Objects.requireNonNull(whereClauses);
151 
152         mWhereClausesForUpdate = whereClauses;
153         return this;
154     }
155 
getReadRequest()156     public ReadTableRequest getReadRequest() {
157         return new ReadTableRequest(getTable()).setWhereClause(getReadWhereClauses());
158     }
159 
getReadRequestUsingUpdateClause()160     public ReadTableRequest getReadRequestUsingUpdateClause() {
161         return new ReadTableRequest(getTable()).setWhereClause(getUpdateWhereClauses());
162     }
163 
164     @NonNull
getReadWhereClauses()165     private WhereClauses getReadWhereClauses() {
166         WhereClauses readWhereClause = new WhereClauses(OR);
167 
168         for (Pair<String, Integer> uniqueColumn : mUniqueColumns) {
169             switch (uniqueColumn.second) {
170                  case TYPE_BLOB -> readWhereClause.addWhereEqualsClause(
171                         uniqueColumn.first, StorageUtils.getHexString(
172                                 mContentValues.getAsByteArray(uniqueColumn.first)));
173                  case TYPE_STRING -> readWhereClause.addWhereEqualsClause(
174                          uniqueColumn.first, mContentValues.getAsString(uniqueColumn.first));
175                 default -> throw new UnsupportedOperationException(
176                         "Unable to find type: " + uniqueColumn.second);
177             }
178         }
179 
180         return readWhereClause;
181     }
182 
requiresUpdate(Cursor cursor, UpsertTableRequest request)183     public boolean requiresUpdate(Cursor cursor, UpsertTableRequest request) {
184         return mRequiresUpdate.requiresUpdate(cursor, getContentValues(), request);
185     }
186 
getRowIdColName()187     public String getRowIdColName() {
188         return RecordHelper.PRIMARY_COLUMN_NAME;
189     }
190 
191     @RecordTypeIdentifier.RecordType
getRecordType()192     public int getRecordType() {
193         Objects.requireNonNull(mRecordType);
194         return mRecordType;
195     }
196 
setRecordType(@ecordTypeIdentifier.RecordType int recordIdentifier)197     public void setRecordType(@RecordTypeIdentifier.RecordType int recordIdentifier) {
198         mRecordType = recordIdentifier;
199     }
200 
setHelper( RecordHelper<?> recordHelper)201     public <T extends RecordInternal<?>> UpsertTableRequest setHelper(
202             RecordHelper<?> recordHelper) {
203         mRecordHelper = recordHelper;
204 
205         return this;
206     }
207 
208     @NonNull
getChildTablesWithRowsToBeDeletedDuringUpdate()209     public List<RecordHelper.TableColumnPair> getChildTablesWithRowsToBeDeletedDuringUpdate() {
210         return mRecordHelper == null
211                 ? Collections.emptyList()
212                 : mRecordHelper.getChildTablesWithRowsToBeDeletedDuringUpdate(
213                         mExtraWritePermissionsStateMapping);
214     }
215 
216     @NonNull
getAllChildTables()217     public List<String> getAllChildTables() {
218         return mRecordHelper == null ? Collections.emptyList() : mRecordHelper.getAllChildTables();
219     }
220 
getRecordInternal()221     public RecordInternal<?> getRecordInternal() {
222         return mRecordInternal;
223     }
224 
setRecordInternal(RecordInternal<?> recordInternal)225     public void setRecordInternal(RecordInternal<?> recordInternal) {
226         mRecordInternal = recordInternal;
227     }
228 
setExtraWritePermissionsStateMapping( @ullable ArrayMap<String, Boolean> extraWritePermissionsToState)229     public <T extends RecordInternal<?>> UpsertTableRequest setExtraWritePermissionsStateMapping(
230             @Nullable ArrayMap<String, Boolean> extraWritePermissionsToState) {
231         mExtraWritePermissionsStateMapping = extraWritePermissionsToState;
232         return this;
233     }
234 
235     /** Get SQL commands to be exected after this upsert has completed. */
getPostUpsertCommands()236     public List<String> getPostUpsertCommands() {
237         return mPostUpsertCommands;
238     }
239 
240     /** Set SQL commands to be exected after this upsert has completed. */
setPostUpsertCommands(List<String> commands)241     public UpsertTableRequest setPostUpsertCommands(List<String> commands) {
242         mPostUpsertCommands = commands;
243         return this;
244     }
245 
246     @Target(ElementType.TYPE_USE)
247     @Retention(RetentionPolicy.SOURCE)
248     @IntDef({TYPE_STRING, TYPE_BLOB})
249     public @interface ColumnType {}
250 
251     public interface IRequiresUpdate {
requiresUpdate( Cursor cursor, ContentValues contentValues, UpsertTableRequest request)252         default boolean requiresUpdate(
253                 Cursor cursor, ContentValues contentValues, UpsertTableRequest request) {
254             return true;
255         }
256     }
257 }
258