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 android.healthconnect.cts.phr.utils;
18 
19 import static android.health.connect.HealthPermissions.WRITE_MEDICAL_DATA;
20 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_ALLERGIES_INTOLERANCES;
21 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_CONDITIONS;
22 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_LABORATORY_RESULTS;
23 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_MEDICATIONS;
24 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_PERSONAL_DETAILS;
25 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_PRACTITIONER_DETAILS;
26 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_PREGNANCY;
27 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_PROCEDURES;
28 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_SOCIAL_HISTORY;
29 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_VACCINES;
30 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_VISITS;
31 import static android.health.connect.datatypes.MedicalResource.MEDICAL_RESOURCE_TYPE_VITAL_SIGNS;
32 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_ALLERGY;
33 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_CONDITION;
34 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_ENCOUNTER;
35 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_IMMUNIZATION;
36 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_MEDICATION;
37 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_OBSERVATION_LABS;
38 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_OBSERVATION_PREGNANCY;
39 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_OBSERVATION_SOCIAL_HISTORY;
40 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_OBSERVATION_VITAL_SIGNS;
41 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_PRACTITIONER;
42 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_PROCEDURE;
43 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_DATA_Patient;
44 import static android.healthconnect.cts.phr.utils.PhrDataFactory.FHIR_VERSION_R4;
45 import static android.healthconnect.cts.phr.utils.PhrDataFactory.createVaccineMedicalResources;
46 import static android.healthconnect.cts.phr.utils.PhrDataFactory.getCreateMedicalDataSourceRequest;
47 import static android.healthconnect.cts.utils.PermissionHelper.MANAGE_HEALTH_DATA;
48 import static android.healthconnect.cts.utils.PermissionHelper.grantPermission;
49 import static android.healthconnect.cts.utils.PermissionHelper.revokePermission;
50 
51 import static com.android.healthfitness.flags.AconfigFlagHelper.isPersonalHealthRecordEnabled;
52 
53 import static com.google.common.base.Preconditions.checkState;
54 
55 import static java.util.stream.Collectors.toSet;
56 
57 import android.app.UiAutomation;
58 import android.health.connect.CreateMedicalDataSourceRequest;
59 import android.health.connect.DeleteMedicalResourcesRequest;
60 import android.health.connect.GetMedicalDataSourcesRequest;
61 import android.health.connect.HealthConnectManager;
62 import android.health.connect.MedicalResourceId;
63 import android.health.connect.ReadMedicalResourcesRequest;
64 import android.health.connect.ReadMedicalResourcesResponse;
65 import android.health.connect.UpsertMedicalResourceRequest;
66 import android.health.connect.datatypes.MedicalDataSource;
67 import android.health.connect.datatypes.MedicalResource;
68 import android.healthconnect.cts.lib.TestAppProxy;
69 import android.healthconnect.cts.utils.HealthConnectReceiver;
70 import android.os.OutcomeReceiver;
71 
72 import androidx.test.platform.app.InstrumentationRegistry;
73 
74 import com.google.common.collect.Iterables;
75 
76 import java.time.Duration;
77 import java.time.Instant;
78 import java.util.ArrayList;
79 import java.util.List;
80 import java.util.Set;
81 import java.util.concurrent.Executor;
82 import java.util.concurrent.ExecutorService;
83 import java.util.concurrent.Executors;
84 
85 public class PhrCtsTestUtils {
86 
87     public static final int MAX_FOREGROUND_READ_CALL_15M = 2000;
88     public static final int MAX_FOREGROUND_WRITE_CALL_15M = 1000;
89     public static final int RECORD_SIZE_LIMIT_IN_BYTES = 1000000;
90     public static final int CHUNK_SIZE_LIMIT_IN_BYTES = 5000000;
91     private static final int MAX_NUMBER_OF_MEDICAL_RESOURCES_PER_INSERT_REQUEST = 20;
92     private static final int MAXIMUM_PAGE_SIZE = 5000;
93     public static final String PHR_BACKGROUND_APP_PKG =
94             "android.healthconnect.cts.phr.testhelper.app1";
95     public static final String PHR_FOREGROUND_APP_PKG =
96             "android.healthconnect.cts.phr.testhelper.app2";
97     public static final TestAppProxy PHR_BACKGROUND_APP =
98             TestAppProxy.forPackageNameInBackground(PHR_BACKGROUND_APP_PKG);
99     public static final TestAppProxy PHR_FOREGROUND_APP =
100             TestAppProxy.forPackageName(PHR_FOREGROUND_APP_PKG);
101 
102     public static final Set<Integer> MEDICAL_RESOURCE_TYPES_LIST =
103             Set.of(
104                     MEDICAL_RESOURCE_TYPE_VACCINES,
105                     MEDICAL_RESOURCE_TYPE_ALLERGIES_INTOLERANCES,
106                     MEDICAL_RESOURCE_TYPE_CONDITIONS,
107                     MEDICAL_RESOURCE_TYPE_MEDICATIONS,
108                     MEDICAL_RESOURCE_TYPE_PERSONAL_DETAILS,
109                     MEDICAL_RESOURCE_TYPE_PRACTITIONER_DETAILS,
110                     MEDICAL_RESOURCE_TYPE_VISITS,
111                     MEDICAL_RESOURCE_TYPE_PROCEDURES,
112                     MEDICAL_RESOURCE_TYPE_PREGNANCY,
113                     MEDICAL_RESOURCE_TYPE_SOCIAL_HISTORY,
114                     MEDICAL_RESOURCE_TYPE_VITAL_SIGNS,
115                     MEDICAL_RESOURCE_TYPE_LABORATORY_RESULTS);
116 
117     public int mLimitsAdjustmentForTesting = 1;
118     private final HealthConnectManager mManager;
119 
PhrCtsTestUtils(HealthConnectManager manager)120     public PhrCtsTestUtils(HealthConnectManager manager) {
121         mManager = manager;
122     }
123 
124     /**
125      * Makes a call to {@link HealthConnectManager#createMedicalDataSource} and returns the created
126      * data source.
127      */
createDataSource(CreateMedicalDataSourceRequest createRequest)128     public MedicalDataSource createDataSource(CreateMedicalDataSourceRequest createRequest)
129             throws InterruptedException {
130         HealthConnectReceiver<MedicalDataSource> createReceiver = new HealthConnectReceiver<>();
131         mManager.createMedicalDataSource(
132                 createRequest, Executors.newSingleThreadExecutor(), createReceiver);
133         return createReceiver.getResponse();
134     }
135 
136     /**
137      * Makes a call to {@link HealthConnectManager#getMedicalDataSources(List, Executor,
138      * OutcomeReceiver)}.
139      */
getMedicalDataSourcesByIds(List<String> ids)140     public List<MedicalDataSource> getMedicalDataSourcesByIds(List<String> ids)
141             throws InterruptedException {
142         HealthConnectReceiver<List<MedicalDataSource>> createReceiver =
143                 new HealthConnectReceiver<>();
144         mManager.getMedicalDataSources(ids, Executors.newSingleThreadExecutor(), createReceiver);
145         return createReceiver.getResponse();
146     }
147 
148     /**
149      * Makes a call to {@link
150      * HealthConnectManager#getMedicalDataSources(GetMedicalDataSourcesRequest, Executor,
151      * OutcomeReceiver)}.
152      */
getMedicalDataSourcesByRequest( GetMedicalDataSourcesRequest request)153     public List<MedicalDataSource> getMedicalDataSourcesByRequest(
154             GetMedicalDataSourcesRequest request) throws InterruptedException {
155         HealthConnectReceiver<List<MedicalDataSource>> createReceiver =
156                 new HealthConnectReceiver<>();
157         mManager.getMedicalDataSources(
158                 request, Executors.newSingleThreadExecutor(), createReceiver);
159         return createReceiver.getResponse();
160     }
161 
162     /**
163      * Given a {@code dataSourceId} and {@code numOfResources}, it inserts as many vaccine medical
164      * resources as specified.
165      */
upsertVaccineMedicalResources( String dataSourceId, int numOfResources)166     public List<MedicalResource> upsertVaccineMedicalResources(
167             String dataSourceId, int numOfResources) throws InterruptedException {
168         List<MedicalResource> medicalResources =
169                 createVaccineMedicalResources(numOfResources, dataSourceId);
170         return upsertMedicalData(medicalResources);
171     }
172 
upsertMedicalData(List<MedicalResource> medicalResources)173     private List<MedicalResource> upsertMedicalData(List<MedicalResource> medicalResources)
174             throws InterruptedException {
175         int numOfResources = medicalResources.size();
176         // To avoid hitting transaction limit:
177         List<MedicalResource> result = new ArrayList<>();
178         for (int chunk = 0;
179                 chunk <= numOfResources / MAX_NUMBER_OF_MEDICAL_RESOURCES_PER_INSERT_REQUEST;
180                 chunk++) {
181             List<UpsertMedicalResourceRequest> requests = new ArrayList<>();
182             HealthConnectReceiver<List<MedicalResource>> dataReceiver =
183                     new HealthConnectReceiver<>();
184             for (int indexWithinChunk = 0;
185                     indexWithinChunk < MAX_NUMBER_OF_MEDICAL_RESOURCES_PER_INSERT_REQUEST;
186                     indexWithinChunk++) {
187                 int index =
188                         chunk * MAX_NUMBER_OF_MEDICAL_RESOURCES_PER_INSERT_REQUEST
189                                 + indexWithinChunk;
190                 if (index >= numOfResources) {
191                     break;
192                 }
193                 MedicalResource medicalResource = medicalResources.get(index);
194                 UpsertMedicalResourceRequest request =
195                         new UpsertMedicalResourceRequest.Builder(
196                                         medicalResource.getDataSourceId(),
197                                         medicalResource.getFhirVersion(),
198                                         medicalResource.getFhirResource().getData())
199                                 .build();
200                 requests.add(request);
201             }
202             mManager.upsertMedicalResources(
203                     requests, Executors.newSingleThreadExecutor(), dataReceiver);
204             result.addAll(dataReceiver.getResponse());
205         }
206         return result;
207     }
208 
209     /**
210      * Makes a call to {@link HealthConnectManager#upsertMedicalResources} and returns the upserted
211      * medical resource.
212      */
upsertMedicalData(String dataSourceId, String data)213     public MedicalResource upsertMedicalData(String dataSourceId, String data)
214             throws InterruptedException {
215         HealthConnectReceiver<List<MedicalResource>> dataReceiver = new HealthConnectReceiver<>();
216         UpsertMedicalResourceRequest request =
217                 new UpsertMedicalResourceRequest.Builder(dataSourceId, FHIR_VERSION_R4, data)
218                         .build();
219         mManager.upsertMedicalResources(
220                 List.of(request), Executors.newSingleThreadExecutor(), dataReceiver);
221         // Make sure something got inserted.
222         return Iterables.getOnlyElement(dataReceiver.getResponse());
223     }
224 
225     /** Makes a call to {@link HealthConnectManager#deleteMedicalResources}. */
deleteResources(List<MedicalResourceId> resourceIds)226     public void deleteResources(List<MedicalResourceId> resourceIds) throws InterruptedException {
227         HealthConnectReceiver<Void> deleteReceiver = new HealthConnectReceiver<>();
228         mManager.deleteMedicalResources(
229                 resourceIds, Executors.newSingleThreadExecutor(), deleteReceiver);
230         deleteReceiver.verifyNoExceptionOrThrow();
231     }
232 
233     /**
234      * A utility method to call {@link HealthConnectManager#readMedicalResources(List, Executor,
235      * OutcomeReceiver)}.
236      */
readMedicalResourcesByIds(List<MedicalResourceId> ids)237     public List<MedicalResource> readMedicalResourcesByIds(List<MedicalResourceId> ids)
238             throws InterruptedException {
239         HealthConnectReceiver<List<MedicalResource>> dataReceiver = new HealthConnectReceiver<>();
240         mManager.readMedicalResources(ids, Executors.newSingleThreadExecutor(), dataReceiver);
241         return dataReceiver.getResponse();
242     }
243 
244     /**
245      * A utility method to call {@link
246      * HealthConnectManager#readMedicalResources(ReadMedicalResourcesRequest, Executor,
247      * OutcomeReceiver)}.
248      */
readMedicalResourcesByRequest( ReadMedicalResourcesRequest request)249     public ReadMedicalResourcesResponse readMedicalResourcesByRequest(
250             ReadMedicalResourcesRequest request) throws InterruptedException {
251         HealthConnectReceiver<ReadMedicalResourcesResponse> dataReceiver =
252                 new HealthConnectReceiver<>();
253         mManager.readMedicalResources(request, Executors.newSingleThreadExecutor(), dataReceiver);
254         return dataReceiver.getResponse();
255     }
256 
257     /**
258      * A utility method to call {@link HealthConnectManager#deleteMedicalResources(List, Executor,
259      * OutcomeReceiver)}.
260      */
deleteMedicalResourcesByIds(List<MedicalResourceId> ids)261     public void deleteMedicalResourcesByIds(List<MedicalResourceId> ids)
262             throws InterruptedException {
263         HealthConnectReceiver<Void> dataReceiver = new HealthConnectReceiver<>();
264         mManager.deleteMedicalResources(ids, Executors.newSingleThreadExecutor(), dataReceiver);
265         dataReceiver.getResponse();
266     }
267 
268     /**
269      * A utility method to call {@link
270      * HealthConnectManager#deleteMedicalResources(DeleteMedicalResourcesRequest, Executor,
271      * OutcomeReceiver)}.
272      */
deleteMedicalResourcesByRequest(DeleteMedicalResourcesRequest request)273     public void deleteMedicalResourcesByRequest(DeleteMedicalResourcesRequest request)
274             throws InterruptedException {
275         HealthConnectReceiver<Void> dataReceiver = new HealthConnectReceiver<>();
276         mManager.deleteMedicalResources(request, Executors.newSingleThreadExecutor(), dataReceiver);
277         dataReceiver.getResponse();
278     }
279 
280     /**
281      * A utility method to call {@link
282      * HealthConnectManager#deleteMedicalResources(DeleteMedicalResourcesRequest, Executor,
283      * OutcomeReceiver)}.
284      */
readMedicalResourcesByRequest(DeleteMedicalResourcesRequest request)285     public void readMedicalResourcesByRequest(DeleteMedicalResourcesRequest request)
286             throws InterruptedException {
287         HealthConnectReceiver<Void> dataReceiver = new HealthConnectReceiver<>();
288         mManager.deleteMedicalResources(request, Executors.newSingleThreadExecutor(), dataReceiver);
289         dataReceiver.getResponse();
290     }
291 
292     /**
293      * Delete all health records (data sources, resources etc) stored in the Health Connect
294      * database.
295      */
deleteAllMedicalData()296     public void deleteAllMedicalData() throws InterruptedException {
297         if (!isPersonalHealthRecordEnabled()) {
298             return;
299         }
300         UiAutomation uiAutomation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
301         uiAutomation.adoptShellPermissionIdentity(MANAGE_HEALTH_DATA);
302         try {
303             HealthConnectReceiver<List<MedicalDataSource>> receiver = new HealthConnectReceiver<>();
304             ExecutorService executor = Executors.newSingleThreadExecutor();
305             mManager.getMedicalDataSources(
306                     new GetMedicalDataSourcesRequest.Builder().build(), executor, receiver);
307             List<MedicalDataSource> dataSources = receiver.getResponse();
308             for (MedicalDataSource dataSource : dataSources) {
309                 HealthConnectReceiver<Void> callback = new HealthConnectReceiver<>();
310                 mManager.deleteMedicalDataSourceWithData(dataSource.getId(), executor, callback);
311                 callback.verifyNoExceptionOrThrow();
312             }
313         } finally {
314             uiAutomation.dropShellPermissionIdentity();
315         }
316     }
317 
318     /**
319      * Given a list of {@link MedicalResource}s, reads out the resources using the {@link
320      * MedicalResourceId}s. It splits the resources to fit the maximum page size limit.
321      */
readMedicalResources(List<MedicalResource> medicalResources)322     public List<MedicalResource> readMedicalResources(List<MedicalResource> medicalResources)
323             throws InterruptedException {
324         List<MedicalResourceId> ids =
325                 medicalResources.stream()
326                         .map(
327                                 medicalResource ->
328                                         new MedicalResourceId(
329                                                 medicalResource.getDataSourceId(),
330                                                 medicalResource.getFhirResource().getType(),
331                                                 medicalResource.getFhirResource().getId()))
332                         .toList();
333 
334         List<MedicalResource> result = new ArrayList<>();
335         for (int chunk = 0; chunk <= ids.size() / MAXIMUM_PAGE_SIZE; chunk++) {
336             List<MedicalResourceId> resourceIds = new ArrayList<>();
337             HealthConnectReceiver<List<MedicalResource>> dataReceiver =
338                     new HealthConnectReceiver<>();
339             for (int indexWithinChunk = 0;
340                     indexWithinChunk < MAXIMUM_PAGE_SIZE;
341                     indexWithinChunk++) {
342                 int index = chunk * MAXIMUM_PAGE_SIZE + indexWithinChunk;
343                 if (index >= ids.size()) {
344                     break;
345                 }
346 
347                 resourceIds.add(ids.get(index));
348             }
349             mManager.readMedicalResources(
350                     resourceIds, Executors.newSingleThreadExecutor(), dataReceiver);
351             result.addAll(dataReceiver.getResponse());
352         }
353         return result;
354     }
355 
356     /**
357      * Given a {@code dataSourceId} deletes the {@link MedicalDataSource} and all its associated
358      * {@link MedicalResource}s.
359      */
deleteMedicalDataSourceWithData(String dataSourceId)360     public void deleteMedicalDataSourceWithData(String dataSourceId) throws InterruptedException {
361         HealthConnectReceiver<Void> callback = new HealthConnectReceiver<>();
362         mManager.deleteMedicalDataSourceWithData(
363                 dataSourceId, Executors.newSingleThreadExecutor(), callback);
364         callback.verifyNoExceptionOrThrow();
365     }
366 
367     /**
368      * Inserts a data source with one resource for each permission category and returns the ids of
369      * inserted resources.
370      */
insertSourceAndOneResourcePerPermissionCategory( TestAppProxy appProxy)371     public List<MedicalResourceId> insertSourceAndOneResourcePerPermissionCategory(
372             TestAppProxy appProxy) throws Exception {
373         grantPermission(appProxy.getPackageName(), WRITE_MEDICAL_DATA);
374         String dataSourceId =
375                 appProxy.createMedicalDataSource(getCreateMedicalDataSourceRequest()).getId();
376         List<MedicalResource> insertedMedicalResources =
377                 List.of(
378                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_IMMUNIZATION),
379                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_ALLERGY),
380                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_CONDITION),
381                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_MEDICATION),
382                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_Patient),
383                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_PRACTITIONER),
384                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_ENCOUNTER),
385                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_PROCEDURE),
386                         appProxy.upsertMedicalResource(
387                                 dataSourceId, FHIR_DATA_OBSERVATION_PREGNANCY),
388                         appProxy.upsertMedicalResource(
389                                 dataSourceId, FHIR_DATA_OBSERVATION_SOCIAL_HISTORY),
390                         appProxy.upsertMedicalResource(
391                                 dataSourceId, FHIR_DATA_OBSERVATION_VITAL_SIGNS),
392                         appProxy.upsertMedicalResource(dataSourceId, FHIR_DATA_OBSERVATION_LABS));
393         revokePermission(appProxy.getPackageName(), WRITE_MEDICAL_DATA);
394 
395         checkState(
396                 insertedMedicalResources.stream()
397                         .map(MedicalResource::getType)
398                         .collect(toSet())
399                         .equals(MEDICAL_RESOURCE_TYPES_LIST));
400 
401         return insertedMedicalResources.stream().map(MedicalResource::getId).toList();
402     }
403 
404     /**
405      * This method tries to use the specified quota by calling readMedicalResources and
406      * getMedicalResources APIs.
407      *
408      * @return the available quota that may have accumulated during the read.
409      */
tryAcquireCallQuotaNTimesForRead( MedicalDataSource insertedMedicalDataSource, List<MedicalResource> insertedMedicalResources, int nTimes)410     public float tryAcquireCallQuotaNTimesForRead(
411             MedicalDataSource insertedMedicalDataSource,
412             List<MedicalResource> insertedMedicalResources,
413             int nTimes)
414             throws InterruptedException {
415         int readMedicalDataSourceCalls = nTimes / 2;
416         int readMedicalResourceCalls = nTimes - readMedicalDataSourceCalls;
417         List<MedicalResourceId> medicalResourceIds =
418                 insertedMedicalResources.stream().map(MedicalResource::getId).toList();
419 
420         Instant readStartTime = Instant.now();
421         for (int i = 0; i < readMedicalDataSourceCalls; i++) {
422             getMedicalDataSourcesByIds(List.of(insertedMedicalDataSource.getId()));
423         }
424         for (int i = 0; i < readMedicalResourceCalls; i++) {
425             readMedicalResourcesByIds(medicalResourceIds);
426         }
427         Instant readEndTime = Instant.now();
428 
429         return getAvailableQuotaAccumulated(
430                 readStartTime, readEndTime, Duration.ofMinutes(15), MAX_FOREGROUND_READ_CALL_15M);
431     }
432 
433     /**
434      * This method tries to use the specified quota by calling the upsertMedicalResources API.
435      *
436      * @return the available quota that may have accumulated during the write.
437      */
tryAcquireCallQuotaNTimesForWrite( MedicalDataSource insertedMedicalDataSource, int nTimes)438     public float tryAcquireCallQuotaNTimesForWrite(
439             MedicalDataSource insertedMedicalDataSource, int nTimes) throws InterruptedException {
440         String dataSourceId = insertedMedicalDataSource.getId();
441 
442         Instant readStartTime = Instant.now();
443         for (int i = 0; i < nTimes; i++) {
444             upsertMedicalData(dataSourceId, FHIR_DATA_IMMUNIZATION);
445         }
446         Instant readEndTime = Instant.now();
447 
448         return getAvailableQuotaAccumulated(
449                 readStartTime, readEndTime, Duration.ofMinutes(15), MAX_FOREGROUND_WRITE_CALL_15M);
450     }
451 
452     /**
453      * Returns the quota that would have accumulated between start and end time for the specified
454      * window and max quota. The calculation matches the calculation in
455      * RateLimiter#getAvailableQuota.
456      */
getAvailableQuotaAccumulated( Instant startTime, Instant endTime, Duration window, int maxQuota)457     private float getAvailableQuotaAccumulated(
458             Instant startTime, Instant endTime, Duration window, int maxQuota) {
459         Duration timeSpent = Duration.between(startTime, endTime);
460         return timeSpent.toMillis() * ((float) maxQuota / (float) window.toMillis());
461     }
462 }
463