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.datatypehelpers;
18 
19 import static android.health.connect.Constants.DEFAULT_LONG;
20 
21 import static com.android.server.healthconnect.storage.utils.StorageUtils.INTEGER;
22 import static com.android.server.healthconnect.storage.utils.StorageUtils.PRIMARY;
23 import static com.android.server.healthconnect.storage.utils.StorageUtils.TEXT_NULL;
24 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorInt;
25 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong;
26 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorString;
27 
28 import android.annotation.NonNull;
29 import android.content.ContentValues;
30 import android.database.Cursor;
31 import android.health.connect.datatypes.Device.DeviceType;
32 import android.health.connect.internal.datatypes.RecordInternal;
33 import android.util.Pair;
34 
35 import com.android.server.healthconnect.storage.TransactionManager;
36 import com.android.server.healthconnect.storage.request.CreateTableRequest;
37 import com.android.server.healthconnect.storage.request.ReadTableRequest;
38 import com.android.server.healthconnect.storage.request.UpsertTableRequest;
39 
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Objects;
44 import java.util.concurrent.ConcurrentHashMap;
45 
46 /**
47  * A class to help with the DB transaction for storing Device Info. {@link DeviceInfoHelper} acts as
48  * a layer b/w the device_info_table stored in the DB and helps perform insert and read operations
49  * on the table
50  *
51  * @hide
52  */
53 public class DeviceInfoHelper extends DatabaseHelper {
54     private static final String TABLE_NAME = "device_info_table";
55     private static final String MANUFACTURER_COLUMN_NAME = "manufacturer";
56     private static final String MODEL_COLUMN_NAME = "model";
57     private static final String DEVICE_TYPE_COLUMN_NAME = "device_type";
58 
59     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
60     private static volatile DeviceInfoHelper sDeviceInfoHelper;
61 
62     /** Map to store deviceInfoId -> DeviceInfo mapping for populating record for read */
63     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
64     private volatile ConcurrentHashMap<Long, DeviceInfo> mIdDeviceInfoMap;
65 
66     /** ArrayMap to store DeviceInfo -> rowId mapping (model,manufacturer,device_type -> rowId) */
67     @SuppressWarnings("NullAway.Init") // TODO(b/317029272): fix this suppression
68     private volatile ConcurrentHashMap<DeviceInfo, Long> mDeviceInfoMap;
69 
70     /**
71      * Returns a requests representing the tables that should be created corresponding to this
72      * helper
73      */
74     @NonNull
getCreateTableRequest()75     public static CreateTableRequest getCreateTableRequest() {
76         return new CreateTableRequest(TABLE_NAME, getColumnInfo());
77     }
78 
getTableName()79     public String getTableName() {
80         return TABLE_NAME;
81     }
82 
83     /** Populates record with deviceInfoId */
populateDeviceInfoId(@onNull RecordInternal<?> recordInternal)84     public void populateDeviceInfoId(@NonNull RecordInternal<?> recordInternal) {
85         String manufacturer = recordInternal.getManufacturer();
86         String model = recordInternal.getModel();
87         int deviceType = recordInternal.getDeviceType();
88         DeviceInfo deviceInfo = new DeviceInfo(manufacturer, model, deviceType);
89         long rowId = getDeviceInfoMap().getOrDefault(deviceInfo, DEFAULT_LONG);
90         if (rowId == DEFAULT_LONG) {
91             rowId = insertIfNotPresent(deviceInfo);
92         }
93         recordInternal.setDeviceInfoId(rowId);
94     }
95 
96     /**
97      * Populates record with manufacturer, model and deviceType values
98      *
99      * @param deviceInfoId rowId from {@code device_info_table }
100      * @param record The record to be populated with values
101      */
populateRecordWithValue(long deviceInfoId, @NonNull RecordInternal<?> record)102     public void populateRecordWithValue(long deviceInfoId, @NonNull RecordInternal<?> record) {
103         DeviceInfo deviceInfo = getIdDeviceInfoMap().get(deviceInfoId);
104         if (deviceInfo != null) {
105             record.setDeviceType(deviceInfo.mDeviceType);
106             record.setManufacturer(deviceInfo.mManufacturer);
107             record.setModel(deviceInfo.mModel);
108         }
109     }
110 
111     @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression
112     @Override
clearCache()113     public synchronized void clearCache() {
114         mDeviceInfoMap = null;
115         mIdDeviceInfoMap = null;
116     }
117 
118     @Override
getMainTableName()119     protected String getMainTableName() {
120         return TABLE_NAME;
121     }
122 
populateDeviceInfoMap()123     private synchronized void populateDeviceInfoMap() {
124         if (mDeviceInfoMap != null) {
125             return;
126         }
127 
128         ConcurrentHashMap<DeviceInfo, Long> deviceInfoMap = new ConcurrentHashMap<>();
129         ConcurrentHashMap<Long, DeviceInfo> idDeviceInfoMap = new ConcurrentHashMap<>();
130         final TransactionManager transactionManager = TransactionManager.getInitialisedInstance();
131         try (Cursor cursor = transactionManager.read(new ReadTableRequest(TABLE_NAME))) {
132             while (cursor.moveToNext()) {
133                 long rowId = getCursorLong(cursor, RecordHelper.PRIMARY_COLUMN_NAME);
134                 String manufacturer = getCursorString(cursor, MANUFACTURER_COLUMN_NAME);
135                 String model = getCursorString(cursor, MODEL_COLUMN_NAME);
136                 int deviceType = getCursorInt(cursor, DEVICE_TYPE_COLUMN_NAME);
137                 DeviceInfo deviceInfo = new DeviceInfo(manufacturer, model, deviceType);
138                 deviceInfoMap.put(deviceInfo, rowId);
139                 idDeviceInfoMap.put(rowId, deviceInfo);
140             }
141         }
142 
143         mDeviceInfoMap = deviceInfoMap;
144         mIdDeviceInfoMap = idDeviceInfoMap;
145     }
146 
getIdDeviceInfoMap()147     private Map<Long, DeviceInfo> getIdDeviceInfoMap() {
148         if (mIdDeviceInfoMap == null) {
149             populateDeviceInfoMap();
150         }
151         return mIdDeviceInfoMap;
152     }
153 
getDeviceInfoMap()154     private Map<DeviceInfo, Long> getDeviceInfoMap() {
155         if (mDeviceInfoMap == null) {
156             populateDeviceInfoMap();
157         }
158 
159         return mDeviceInfoMap;
160     }
161 
insertIfNotPresent(DeviceInfo deviceInfo)162     private synchronized long insertIfNotPresent(DeviceInfo deviceInfo) {
163         Long currentRowId = getDeviceInfoMap().get(deviceInfo);
164         if (currentRowId != null) {
165             return currentRowId;
166         }
167 
168         long rowId =
169                 TransactionManager.getInitialisedInstance()
170                         .insert(
171                                 new UpsertTableRequest(
172                                         TABLE_NAME,
173                                         getContentValues(
174                                                 deviceInfo.mManufacturer,
175                                                 deviceInfo.mModel,
176                                                 deviceInfo.mDeviceType)));
177         getDeviceInfoMap().put(deviceInfo, rowId);
178         getIdDeviceInfoMap().put(rowId, deviceInfo);
179         return rowId;
180     }
181 
182     @NonNull
getContentValues(String manufacturer, String model, int deviceType)183     private ContentValues getContentValues(String manufacturer, String model, int deviceType) {
184         ContentValues contentValues = new ContentValues();
185 
186         contentValues.put(MANUFACTURER_COLUMN_NAME, manufacturer);
187         contentValues.put(MODEL_COLUMN_NAME, model);
188         contentValues.put(DEVICE_TYPE_COLUMN_NAME, deviceType);
189 
190         return contentValues;
191     }
192 
193     /**
194      * This implementation should return the column names with which the table should be created.
195      *
196      * <p>NOTE: New columns can only be added via onUpgrade. Why? Consider what happens if a table
197      * already exists on the device
198      *
199      * <p>PLEASE DON'T USE THIS METHOD TO ADD NEW COLUMNS
200      */
201     @NonNull
getColumnInfo()202     private static List<Pair<String, String>> getColumnInfo() {
203         ArrayList<Pair<String, String>> columnInfo = new ArrayList<>();
204         columnInfo.add(new Pair<>(RecordHelper.PRIMARY_COLUMN_NAME, PRIMARY));
205         columnInfo.add(new Pair<>(MANUFACTURER_COLUMN_NAME, TEXT_NULL));
206         columnInfo.add(new Pair<>(MODEL_COLUMN_NAME, TEXT_NULL));
207         columnInfo.add(new Pair<>(DEVICE_TYPE_COLUMN_NAME, INTEGER));
208 
209         return columnInfo;
210     }
211 
getInstance()212     public static synchronized DeviceInfoHelper getInstance() {
213         if (sDeviceInfoHelper == null) {
214             sDeviceInfoHelper = new DeviceInfoHelper();
215         }
216         return sDeviceInfoHelper;
217     }
218 
219     private static final class DeviceInfo {
220         private final String mManufacturer;
221         private final String mModel;
222         @DeviceType private final int mDeviceType;
223 
DeviceInfo(String manufacturer, String model, @DeviceType int deviceType)224         DeviceInfo(String manufacturer, String model, @DeviceType int deviceType) {
225             mManufacturer = manufacturer;
226             mModel = model;
227             mDeviceType = deviceType;
228         }
229 
230         @Override
hashCode()231         public int hashCode() {
232             int result = mManufacturer != null ? mManufacturer.hashCode() : 0;
233             result = 31 * result + (mModel != null ? mModel.hashCode() : 0) + mDeviceType;
234             return result;
235         }
236 
237         @Override
equals(Object o)238         public boolean equals(Object o) {
239             if (Objects.isNull(o)) {
240                 return false;
241             }
242             if (this == o) {
243                 return true;
244             }
245             if (getClass() != o.getClass()) {
246                 return false;
247             }
248 
249             DeviceInfo deviceInfo = (DeviceInfo) o;
250             if (!Objects.equals(mManufacturer, deviceInfo.mManufacturer)) {
251                 return false;
252             }
253             if (!Objects.equals(mModel, deviceInfo.mModel)) {
254                 return false;
255             }
256 
257             return mDeviceType == deviceInfo.mDeviceType;
258         }
259     }
260 }
261