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