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