• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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