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