1 /* 2 * Copyright (C) 2024 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.testing.storage; 18 19 import static android.health.connect.Constants.DEFAULT_LONG; 20 import static android.health.connect.Constants.MAXIMUM_PAGE_SIZE; 21 import static android.healthconnect.cts.phr.utils.PhrDataFactory.DATA_SOURCE_DISPLAY_NAME; 22 import static android.healthconnect.cts.phr.utils.PhrDataFactory.DATA_SOURCE_FHIR_BASE_URI; 23 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_VERSION_R4; 24 25 import static com.android.server.healthconnect.storage.datatypehelpers.RecordHelper.LAST_MODIFIED_TIME_COLUMN_NAME; 26 import static com.android.server.healthconnect.storage.utils.StorageUtils.getCursorLong; 27 28 import android.content.Context; 29 import android.database.Cursor; 30 import android.database.sqlite.SQLiteDatabase; 31 import android.health.connect.CreateMedicalDataSourceRequest; 32 import android.health.connect.accesslog.AccessLog; 33 import android.health.connect.datatypes.FhirResource; 34 import android.health.connect.datatypes.FhirVersion; 35 import android.health.connect.datatypes.MedicalDataSource; 36 import android.health.connect.datatypes.MedicalResource; 37 import android.net.Uri; 38 import android.util.Pair; 39 40 import com.android.server.healthconnect.injector.HealthConnectInjector; 41 import com.android.server.healthconnect.phr.PhrPageTokenWrapper; 42 import com.android.server.healthconnect.phr.ReadMedicalResourcesInternalResponse; 43 import com.android.server.healthconnect.storage.HealthConnectDatabase; 44 import com.android.server.healthconnect.storage.TransactionManager; 45 import com.android.server.healthconnect.storage.datatypehelpers.AppInfoHelper; 46 import com.android.server.healthconnect.storage.datatypehelpers.MedicalDataSourceHelper; 47 import com.android.server.healthconnect.storage.datatypehelpers.MedicalResourceHelper; 48 import com.android.server.healthconnect.storage.datatypehelpers.MedicalResourceIndicesHelper; 49 import com.android.server.healthconnect.storage.request.ReadTableRequest; 50 import com.android.server.healthconnect.storage.request.UpsertMedicalResourceInternalRequest; 51 52 import com.google.common.truth.Correspondence; 53 54 import java.time.Instant; 55 import java.util.ArrayList; 56 import java.util.List; 57 import java.util.Objects; 58 import java.util.UUID; 59 60 public class PhrTestUtils { 61 public static final Correspondence<AccessLog, AccessLog> ACCESS_LOG_EQUIVALENCE = 62 Correspondence.from(PhrTestUtils::isAccessLogEqual, "isAccessLogEqual"); 63 64 private final MedicalDataSourceHelper mMedicalDataSourceHelper; 65 private final MedicalResourceHelper mMedicalResourceHelper; 66 private final TransactionManager mTransactionManager; 67 private final AppInfoHelper mAppInfoHelper; 68 PhrTestUtils(HealthConnectInjector healthConnectInjector)69 public PhrTestUtils(HealthConnectInjector healthConnectInjector) { 70 mMedicalResourceHelper = healthConnectInjector.getMedicalResourceHelper(); 71 mMedicalDataSourceHelper = healthConnectInjector.getMedicalDataSourceHelper(); 72 mTransactionManager = healthConnectInjector.getTransactionManager(); 73 mAppInfoHelper = healthConnectInjector.getAppInfoHelper(); 74 } 75 76 /** 77 * Upsert a {@link MedicalResource} using the given {@link MedicalResourceCreator} and the 78 * {@link MedicalDataSource}. 79 */ upsertResource( MedicalResourceCreator creator, MedicalDataSource dataSource)80 public MedicalResource upsertResource( 81 MedicalResourceCreator creator, MedicalDataSource dataSource) { 82 MedicalResource medicalResource = creator.create(dataSource.getId()); 83 return mMedicalResourceHelper 84 .upsertMedicalResources( 85 dataSource.getPackageName(), List.of(makeUpsertRequest(medicalResource))) 86 .get(0); 87 } 88 89 /** 90 * Upsert {@link MedicalResource}s using the given {@link MedicalResourcesCreator}, the {@code 91 * numOfResources} and {@link MedicalDataSource}. 92 */ upsertResources( MedicalResourcesCreator creator, int numOfResources, MedicalDataSource dataSource)93 public List<MedicalResource> upsertResources( 94 MedicalResourcesCreator creator, int numOfResources, MedicalDataSource dataSource) { 95 List<MedicalResource> medicalResources = creator.create(numOfResources, dataSource.getId()); 96 return mMedicalResourceHelper.upsertMedicalResources( 97 dataSource.getPackageName(), 98 medicalResources.stream().map(PhrTestUtils::makeUpsertRequest).toList()); 99 } 100 101 /** Returns a request to upsert the given {@link MedicalResource}. */ makeUpsertRequest(MedicalResource resource)102 public static UpsertMedicalResourceInternalRequest makeUpsertRequest(MedicalResource resource) { 103 return makeUpsertRequest( 104 resource.getFhirResource(), 105 resource.getType(), 106 resource.getFhirVersion(), 107 resource.getDataSourceId()); 108 } 109 110 /** 111 * Returns a request to upsert the given {@link FhirResource}, along with required source 112 * information. 113 */ makeUpsertRequest( FhirResource resource, int medicalResourceType, FhirVersion fhirVersion, String datasourceId)114 public static UpsertMedicalResourceInternalRequest makeUpsertRequest( 115 FhirResource resource, 116 int medicalResourceType, 117 FhirVersion fhirVersion, 118 String datasourceId) { 119 return new UpsertMedicalResourceInternalRequest() 120 .setMedicalResourceType(medicalResourceType) 121 .setFhirResourceId(resource.getId()) 122 .setFhirResourceType(resource.getType()) 123 .setFhirVersion(fhirVersion) 124 .setData(resource.getData()) 125 .setDataSourceId(datasourceId); 126 } 127 128 /** 129 * Insert and return a {@link MedicalDataSource} where the display name, and URI will contain 130 * the given name. 131 * 132 * <p>The FHIR version is set to R4. 133 */ insertR4MedicalDataSource(String name, String packageName)134 public MedicalDataSource insertR4MedicalDataSource(String name, String packageName) { 135 Uri uri = Uri.parse(String.format("%s/%s", DATA_SOURCE_FHIR_BASE_URI, name)); 136 String displayName = String.format("%s %s", DATA_SOURCE_DISPLAY_NAME, name); 137 138 CreateMedicalDataSourceRequest createMedicalDataSourceRequest = 139 new CreateMedicalDataSourceRequest.Builder(uri, displayName, FHIR_VERSION_R4) 140 .build(); 141 return mMedicalDataSourceHelper.createMedicalDataSource( 142 createMedicalDataSourceRequest, packageName); 143 } 144 145 /** Interface for a {@link MedicalResource} creator. */ 146 public interface MedicalResourceCreator { 147 /** Creates a {@link MedicalResource} using the given {@code dataSourceId}. */ create(String dataSourceId)148 MedicalResource create(String dataSourceId); 149 } 150 151 /** Interface for multiple {@link MedicalResource}s creator. */ 152 public interface MedicalResourcesCreator { 153 /** 154 * Creates multiple {@link MedicalResource}s based on the {@code num} and the given {@code 155 * dataSourceId}. 156 */ create(int num, String dataSourceId)157 List<MedicalResource> create(int num, String dataSourceId); 158 } 159 160 /** Reads the last_modified_time column for the given {@code tableName}. */ readLastModifiedTimestamp(String tableName)161 public long readLastModifiedTimestamp(String tableName) { 162 long timestamp = DEFAULT_LONG; 163 ReadTableRequest readTableRequest = new ReadTableRequest(tableName); 164 try (Cursor cursor = mTransactionManager.read(readTableRequest)) { 165 if (cursor.moveToFirst()) { 166 do { 167 timestamp = getCursorLong(cursor, LAST_MODIFIED_TIME_COLUMN_NAME); 168 } while (cursor.moveToNext()); 169 } 170 return timestamp; 171 } 172 } 173 174 /** 175 * Given two {@link AccessLog}s, compare whether they are equal or not. This ignores the {@link 176 * AccessLog#getAccessTime()}. 177 */ isAccessLogEqual(AccessLog actual, AccessLog expected)178 public static boolean isAccessLogEqual(AccessLog actual, AccessLog expected) { 179 return Objects.equals(actual.getPackageName(), expected.getPackageName()) 180 && actual.getOperationType() == expected.getOperationType() 181 && Objects.equals( 182 actual.getMedicalResourceTypes(), expected.getMedicalResourceTypes()) 183 && Objects.equals(actual.getRecordTypes(), expected.getRecordTypes()) 184 && actual.isMedicalDataSourceAccessed() == expected.isMedicalDataSourceAccessed(); 185 } 186 187 /** 188 * Inserts a {@link MedicalDataSource} into the given {@link HealthConnectDatabase} using the 189 * given {@code name}, and {@code packageName}. It returns a pair of rowId of the inserted row 190 * and the generated uuid string of the {@link MedicalDataSource}. 191 */ insertMedicalDataSource( HealthConnectDatabase healthConnectDatabase, Context context, String name, String packageName, Instant instant)192 public Pair<Long, String> insertMedicalDataSource( 193 HealthConnectDatabase healthConnectDatabase, 194 Context context, 195 String name, 196 String packageName, 197 Instant instant) { 198 SQLiteDatabase db = healthConnectDatabase.getWritableDatabase(); 199 long appInfoId = mAppInfoHelper.getOrInsertAppInfoId(db, packageName); 200 if (appInfoId == DEFAULT_LONG) { 201 throw new IllegalStateException("App id does not exist"); 202 } 203 MedicalDataSource dataSource = 204 new MedicalDataSource.Builder( 205 UUID.randomUUID().toString(), 206 packageName, 207 Uri.parse(String.format("%s/%s", DATA_SOURCE_FHIR_BASE_URI, name)), 208 String.format("%s %s", DATA_SOURCE_DISPLAY_NAME, name), 209 FHIR_VERSION_R4) 210 .build(); 211 long rowId = 212 db.insertWithOnConflict( 213 MedicalDataSourceHelper.getMainTableName(), 214 /* nullColumnHack= */ null, 215 MedicalDataSourceHelper.getContentValues( 216 dataSource, appInfoId, instant.toEpochMilli()), 217 SQLiteDatabase.CONFLICT_IGNORE); 218 return new Pair<>(rowId, dataSource.getId()); 219 } 220 221 /** 222 * Inserts a {@code numOfResources} of {@link MedicalResource}s into the given {@link 223 * HealthConnectDatabase} using the given {@link MedicalResourcesCreator}, {@code 224 * dataSourceUuid}, and {@code dataSourceRowId}. 225 */ insertMedicalResources( HealthConnectDatabase healthConnectDatabase, MedicalResourcesCreator creator, String dataSourceUuid, long dataSourceRowId, Instant instant, int numOfResources)226 public void insertMedicalResources( 227 HealthConnectDatabase healthConnectDatabase, 228 MedicalResourcesCreator creator, 229 String dataSourceUuid, 230 long dataSourceRowId, 231 Instant instant, 232 int numOfResources) { 233 List<MedicalResource> medicalResources = creator.create(numOfResources, dataSourceUuid); 234 SQLiteDatabase db = healthConnectDatabase.getWritableDatabase(); 235 for (MedicalResource medicalResource : medicalResources) { 236 insertResource(db, medicalResource, dataSourceRowId, instant); 237 } 238 } 239 240 /** 241 * Inserts a {@link MedicalResource} into the given {@link HealthConnectDatabase} using the 242 * given {@link MedicalResourceCreator}, {@code dataSourceUuid}, and {@code dataSourceRowId}. 243 */ insertMedicalResource( HealthConnectDatabase healthConnectDatabase, MedicalResourceCreator creator, String dataSourceUuid, long dataSourceRowId, Instant instant)244 public void insertMedicalResource( 245 HealthConnectDatabase healthConnectDatabase, 246 MedicalResourceCreator creator, 247 String dataSourceUuid, 248 long dataSourceRowId, 249 Instant instant) { 250 MedicalResource medicalResource = creator.create(dataSourceUuid); 251 SQLiteDatabase db = healthConnectDatabase.getWritableDatabase(); 252 insertResource(db, medicalResource, dataSourceRowId, instant); 253 } 254 insertResource( SQLiteDatabase db, MedicalResource medicalResource, long dataSourceRowId, Instant instant)255 private void insertResource( 256 SQLiteDatabase db, 257 MedicalResource medicalResource, 258 long dataSourceRowId, 259 Instant instant) { 260 long rowId = 261 db.insertWithOnConflict( 262 MedicalResourceHelper.getMainTableName(), 263 /* nullColumnHack= */ null, 264 MedicalResourceHelper.getContentValues( 265 dataSourceRowId, instant.toEpochMilli(), medicalResource), 266 SQLiteDatabase.CONFLICT_REPLACE); 267 db.insertWithOnConflict( 268 MedicalResourceIndicesHelper.getTableName(), 269 /* nullColumnHack= */ null, 270 MedicalResourceIndicesHelper.getContentValues(rowId, medicalResource.getType()), 271 SQLiteDatabase.CONFLICT_REPLACE); 272 } 273 readMedicalResources( SQLiteDatabase db, PhrPageTokenWrapper pageTokenWrapper)274 private static ReadMedicalResourcesInternalResponse readMedicalResources( 275 SQLiteDatabase db, PhrPageTokenWrapper pageTokenWrapper) { 276 ReadTableRequest readTableRequest = 277 MedicalResourceHelper.getReadTableRequestUsingRequestFilters( 278 pageTokenWrapper, MAXIMUM_PAGE_SIZE); 279 return MedicalResourceHelper.getMedicalResources( 280 db, readTableRequest, pageTokenWrapper, MAXIMUM_PAGE_SIZE); 281 } 282 283 /** 284 * Reads all the {@link MedicalResource}s and their associated last_modified_timestamp from the 285 * database. 286 */ readAllMedicalResources()287 public List<Pair<MedicalResource, Long>> readAllMedicalResources() { 288 return mTransactionManager.runWithoutTransaction( 289 (SQLiteDatabase db) -> readAllMedicalResources(db)); 290 } 291 292 /** 293 * Reads all the {@link MedicalResource}s and their associated last_modified_timestamp from the 294 * given {@link HealthConnectDatabase}. 295 */ readAllMedicalResources( HealthConnectDatabase stagedDatabase)296 public static List<Pair<MedicalResource, Long>> readAllMedicalResources( 297 HealthConnectDatabase stagedDatabase) { 298 return readAllMedicalResources(stagedDatabase.getReadableDatabase()); 299 } 300 readAllMedicalResources(SQLiteDatabase db)301 private static List<Pair<MedicalResource, Long>> readAllMedicalResources(SQLiteDatabase db) { 302 List<Pair<MedicalResource, Long>> result = new ArrayList<>(); 303 String nextPageToken = null; 304 do { 305 PhrPageTokenWrapper phrPageTokenWrapper = 306 PhrPageTokenWrapper.fromPageTokenAllowingNull(nextPageToken); 307 ReadMedicalResourcesInternalResponse response = 308 readMedicalResources(db, phrPageTokenWrapper); 309 310 result.addAll( 311 response.getMedicalResources().stream() 312 .map( 313 medicalResource -> 314 new Pair<>( 315 medicalResource, 316 medicalResource.getLastModifiedTimestamp())) 317 .toList()); 318 nextPageToken = response.getPageToken(); 319 320 } while (nextPageToken != null); 321 return result; 322 } 323 324 /** 325 * Reads {@link MedicalDataSource}s and their associated last_modified_timestamp and returns it 326 * as a list of {@link Pair}s with the first element of the pair being {@link MedicalDataSource} 327 * and the second element last_modified_timestamp. 328 */ readMedicalDataSources()329 public List<Pair<MedicalDataSource, Long>> readMedicalDataSources() { 330 try (Cursor cursor = 331 mTransactionManager.rawQuery( 332 MedicalDataSourceHelper.getReadQueryForDataSources(), null)) { 333 return MedicalDataSourceHelper.getMedicalDataSourcesWithTimestamps(cursor); 334 } 335 } 336 337 /** 338 * Reads {@link MedicalDataSource}s and their associated last_modified_timestamp and returns it 339 * as a list of {@link Pair}s with the first element of the pair being {@link MedicalDataSource} 340 * and the second element last_modified_timestamp. 341 */ readMedicalDataSources( HealthConnectDatabase stagedDatabase)342 public static List<Pair<MedicalDataSource, Long>> readMedicalDataSources( 343 HealthConnectDatabase stagedDatabase) { 344 try (Cursor cursor = 345 stagedDatabase 346 .getReadableDatabase() 347 .rawQuery(MedicalDataSourceHelper.getReadQueryForDataSources(), null)) { 348 return MedicalDataSourceHelper.getMedicalDataSourcesWithTimestamps(cursor); 349 } 350 } 351 } 352