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