1 /* 2 * Copyright (C) 2023 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.ratelimiter; 18 19 import static android.health.connect.datatypes.StepsRecord.STEPS_COUNT_TOTAL; 20 import static android.healthconnect.cts.utils.DataFactory.buildDevice; 21 import static android.healthconnect.cts.utils.DataFactory.getCompleteStepsRecord; 22 import static android.healthconnect.cts.utils.DataFactory.getUpdatedStepsRecord; 23 24 import static org.hamcrest.CoreMatchers.containsString; 25 26 import android.app.UiAutomation; 27 import android.content.Context; 28 import android.health.connect.AggregateRecordsRequest; 29 import android.health.connect.DeleteUsingFiltersRequest; 30 import android.health.connect.HealthConnectException; 31 import android.health.connect.ReadRecordsRequestUsingFilters; 32 import android.health.connect.ReadRecordsRequestUsingIds; 33 import android.health.connect.TimeInstantRangeFilter; 34 import android.health.connect.changelog.ChangeLogTokenRequest; 35 import android.health.connect.changelog.ChangeLogTokenResponse; 36 import android.health.connect.changelog.ChangeLogsRequest; 37 import android.health.connect.datatypes.DataOrigin; 38 import android.health.connect.datatypes.Device; 39 import android.health.connect.datatypes.HeartRateRecord; 40 import android.health.connect.datatypes.Metadata; 41 import android.health.connect.datatypes.Record; 42 import android.health.connect.datatypes.StepsRecord; 43 import android.healthconnect.cts.utils.AssumptionCheckerRule; 44 import android.healthconnect.cts.utils.TestUtils; 45 import android.platform.test.annotations.AppModeFull; 46 import android.provider.DeviceConfig; 47 48 import androidx.test.core.app.ApplicationProvider; 49 import androidx.test.platform.app.InstrumentationRegistry; 50 import androidx.test.runner.AndroidJUnit4; 51 52 import com.android.compatibility.common.util.ApiTest; 53 import com.android.modules.utils.build.SdkLevel; 54 55 import org.junit.After; 56 import org.junit.Before; 57 import org.junit.Rule; 58 import org.junit.Test; 59 import org.junit.rules.ExpectedException; 60 import org.junit.runner.RunWith; 61 62 import java.time.Duration; 63 import java.time.Instant; 64 import java.time.temporal.ChronoUnit; 65 import java.util.ArrayList; 66 import java.util.Arrays; 67 import java.util.Collections; 68 import java.util.List; 69 70 @AppModeFull(reason = "HealthConnectManager is not accessible to instant apps") 71 @RunWith(AndroidJUnit4.class) 72 public class RateLimiterTest { 73 private static final String TAG = "RateLimiterTest"; 74 private static final int MAX_FOREGROUND_WRITE_CALL_15M = 1000; 75 private static final int MAX_FOREGROUND_READ_CALL_15M = 2000; 76 private static final Duration WINDOW_15M = Duration.ofMinutes(15); 77 public static final String ENABLE_RATE_LIMITER_FLAG = "enable_rate_limiter"; 78 private final UiAutomation mUiAutomation = 79 InstrumentationRegistry.getInstrumentation().getUiAutomation(); 80 81 @Rule public ExpectedException exception = ExpectedException.none(); 82 83 private int mLimitsAdjustmentForTesting = 1; 84 85 @Rule 86 public AssumptionCheckerRule mSupportedHardwareRule = 87 new AssumptionCheckerRule( 88 TestUtils::isHardwareSupported, "Tests should run on supported hardware only."); 89 90 @Before setUp()91 public void setUp() throws InterruptedException { 92 TestUtils.deleteAllStagedRemoteData(); 93 if (TestUtils.setLowerRateLimitsForTesting(true)) { 94 mLimitsAdjustmentForTesting = 10; 95 } 96 } 97 98 @After tearDown()99 public void tearDown() throws InterruptedException { 100 TestUtils.deleteAllStagedRemoteData(); 101 TestUtils.setLowerRateLimitsForTesting(false); 102 } 103 104 @Test testTryAcquireApiCallQuota_writeCallsInLimit()105 public void testTryAcquireApiCallQuota_writeCallsInLimit() throws InterruptedException { 106 tryAcquireCallQuotaNTimesForWrite(MAX_FOREGROUND_WRITE_CALL_15M); 107 } 108 109 @Test testTryAcquireApiCallQuota_readCallsInLimit()110 public void testTryAcquireApiCallQuota_readCallsInLimit() throws InterruptedException { 111 List<Record> testRecord = List.of(getCompleteStepsRecord()); 112 113 tryAcquireCallQuotaNTimesForRead( 114 testRecord, TestUtils.insertRecords(testRecord), MAX_FOREGROUND_READ_CALL_15M); 115 } 116 117 @Test 118 @ApiTest(apis = {"android.health.connect#insertRecords"}) testTryAcquireApiCallQuota_writeLimitExceeded()119 public void testTryAcquireApiCallQuota_writeLimitExceeded() throws InterruptedException { 120 exception.expect(HealthConnectException.class); 121 exception.expectMessage(containsString("API call quota exceeded")); 122 exceedWriteQuota(); 123 } 124 125 @Test 126 @ApiTest(apis = {"android.health.connect#readRecords"}) testTryAcquireApiCallQuota_readLimitExceeded()127 public void testTryAcquireApiCallQuota_readLimitExceeded() throws InterruptedException { 128 exception.expect(HealthConnectException.class); 129 exception.expectMessage(containsString("API call quota exceeded")); 130 exceedReadQuota(); 131 } 132 133 @Test testChunkSizeLimitExceeded()134 public void testChunkSizeLimitExceeded() throws InterruptedException { 135 exception.expect(HealthConnectException.class); 136 exception.expectMessage(containsString("Records chunk size exceeded the max chunk limit")); 137 exceedChunkMemoryQuota(); 138 } 139 140 @Test testRecordSizeLimitExceeded()141 public void testRecordSizeLimitExceeded() throws InterruptedException { 142 exception.expect(HealthConnectException.class); 143 exception.expectMessage( 144 containsString("Record size exceeded the single record size limit")); 145 exceedRecordMemoryQuota(); 146 } 147 148 @Test 149 @ApiTest(apis = {"android.health.connect#insertRecords"}) testRecordMemoryRollingQuota_foregroundCall_exceedBackgroundLimit()150 public void testRecordMemoryRollingQuota_foregroundCall_exceedBackgroundLimit() 151 throws InterruptedException { 152 // No exception expected. 153 exceedRecordMemoryRollingQuotaBackgroundLimit(); 154 } 155 exceedChunkMemoryQuota()156 private void exceedChunkMemoryQuota() throws InterruptedException { 157 List<Record> testRecord = Collections.nCopies(30000, getCompleteStepsRecord()); 158 159 TestUtils.insertRecords(testRecord); 160 } 161 exceedRecordMemoryQuota()162 private void exceedRecordMemoryQuota() throws InterruptedException { 163 Device device = buildDevice(); 164 DataOrigin dataOrigin = 165 new DataOrigin.Builder().setPackageName("android.healthconnect.cts").build(); 166 Metadata.Builder testMetadataBuilder = new Metadata.Builder(); 167 testMetadataBuilder.setDevice(device).setDataOrigin(dataOrigin); 168 testMetadataBuilder.setClientRecordId("HRR" + Math.random()); 169 testMetadataBuilder.setRecordingMethod(Metadata.RECORDING_METHOD_ACTIVELY_RECORDED); 170 171 HeartRateRecord.HeartRateSample heartRateRecord = 172 new HeartRateRecord.HeartRateSample(10, Instant.now().plusMillis(100)); 173 int nCopies = 85000 / mLimitsAdjustmentForTesting; 174 ArrayList<HeartRateRecord.HeartRateSample> heartRateRecords = 175 new ArrayList<>(Collections.nCopies(nCopies, heartRateRecord)); 176 177 HeartRateRecord testHeartRateRecord = 178 new HeartRateRecord.Builder( 179 testMetadataBuilder.build(), 180 Instant.now(), 181 Instant.now().plusMillis(500), 182 heartRateRecords) 183 .build(); 184 TestUtils.insertRecords(List.of(testHeartRateRecord)); 185 } 186 exceedRecordMemoryRollingQuotaBackgroundLimit()187 private void exceedRecordMemoryRollingQuotaBackgroundLimit() throws InterruptedException { 188 List<Record> testRecord = Collections.nCopies(350, getCompleteStepsRecord()); 189 int nTimes = 1000 / mLimitsAdjustmentForTesting; 190 for (int i = 0; i < nTimes; i++) { 191 TestUtils.insertRecords(testRecord); 192 } 193 } 194 exceedWriteQuota()195 private void exceedWriteQuota() throws InterruptedException { 196 Instant startTime = Instant.now(); 197 tryAcquireCallQuotaNTimesForWrite(MAX_FOREGROUND_WRITE_CALL_15M); 198 Instant endTime = Instant.now(); 199 float quotaAcquired = 200 getQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_WRITE_CALL_15M); 201 List<Record> testRecord = List.of(getCompleteStepsRecord()); 202 203 while (quotaAcquired > 1) { 204 TestUtils.insertRecords(testRecord); 205 quotaAcquired--; 206 } 207 int tryWriteWithBuffer = 20; 208 while (tryWriteWithBuffer > 0) { 209 TestUtils.insertRecords(List.of(getCompleteStepsRecord())); 210 211 tryWriteWithBuffer--; 212 } 213 } 214 exceedReadQuota()215 private void exceedReadQuota() throws InterruptedException { 216 ReadRecordsRequestUsingFilters<StepsRecord> readRecordsRequestUsingFilters = 217 new ReadRecordsRequestUsingFilters.Builder<>(StepsRecord.class) 218 .setAscending(true) 219 .build(); 220 Instant startTime = Instant.now(); 221 List<Record> testRecord = Arrays.asList(getCompleteStepsRecord()); 222 223 List<Record> insertedRecords = TestUtils.insertRecords(testRecord); 224 tryAcquireCallQuotaNTimesForRead(testRecord, insertedRecords, MAX_FOREGROUND_READ_CALL_15M); 225 Instant endTime = Instant.now(); 226 float quotaAcquired = 227 getQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_READ_CALL_15M); 228 while (quotaAcquired > 1) { 229 readStepsRecordUsingIds(insertedRecords); 230 quotaAcquired--; 231 } 232 int tryReadWithBuffer = 20; 233 while (tryReadWithBuffer > 0) { 234 TestUtils.readRecords(readRecordsRequestUsingFilters); 235 tryReadWithBuffer--; 236 } 237 } 238 getQuotaAcquired( Instant startTime, Instant endTime, Duration window, int maxQuota)239 private float getQuotaAcquired( 240 Instant startTime, Instant endTime, Duration window, int maxQuota) { 241 Duration timeSpent = Duration.between(startTime, endTime); 242 return timeSpent.toMillis() * ((float) maxQuota / (float) window.toMillis()); 243 } 244 245 /** 246 * This method tries to use the Maximum read quota possible. Distributes the load to 247 * ChangeLogToken, ChangeLog, Read, and Aggregate APIs. 248 */ tryAcquireCallQuotaNTimesForRead( List<Record> testRecord, List<Record> insertedRecords, int nTimes)249 private void tryAcquireCallQuotaNTimesForRead( 250 List<Record> testRecord, List<Record> insertedRecords, int nTimes) 251 throws InterruptedException { 252 nTimes = nTimes / mLimitsAdjustmentForTesting; 253 Context context = ApplicationProvider.getApplicationContext(); 254 255 // Each getChangelog is 2 reads. 256 int changelogCalls = nTimes / 4; 257 for (int i = 0; i < changelogCalls; i++) { 258 getChangeLog(context); 259 } 260 261 int aggregateCalls = nTimes / 4; 262 AggregateRecordsRequest<Long> aggregateRecordsRequest = 263 new AggregateRecordsRequest.Builder<Long>( 264 new TimeInstantRangeFilter.Builder() 265 .setStartTime(Instant.ofEpochMilli(0)) 266 .setEndTime(Instant.now().plus(1, ChronoUnit.DAYS)) 267 .build()) 268 .addAggregationType(STEPS_COUNT_TOTAL) 269 .build(); 270 for (int i = 0; i < aggregateCalls; i++) { 271 TestUtils.getAggregateResponse(aggregateRecordsRequest, testRecord); 272 } 273 274 for (int i = 0; i < nTimes - aggregateCalls - 2 * changelogCalls; i++) { 275 readStepsRecordUsingIds(insertedRecords); 276 } 277 } 278 getChangeLog(Context context)279 private void getChangeLog(Context context) throws InterruptedException { 280 // Use one read quota. 281 ChangeLogTokenResponse tokenResponse = 282 TestUtils.getChangeLogToken( 283 new ChangeLogTokenRequest.Builder() 284 .addDataOriginFilter( 285 new DataOrigin.Builder() 286 .setPackageName(context.getPackageName()) 287 .build()) 288 .addRecordType(StepsRecord.class) 289 .build()); 290 291 ChangeLogsRequest changeLogsRequest = 292 new ChangeLogsRequest.Builder(tokenResponse.getToken()).build(); 293 // Use one read quota. 294 TestUtils.getChangeLogs(changeLogsRequest); 295 } 296 297 /** 298 * This method tries to use the Maximum write quota possible. Distributes the load across 299 * Insert, and Update APIs. Also, we provide dataManagement permission to 300 * MultiAppTestUtils.verifyDeleteRecords. We test unmetered rate limting as well here. No write 301 * quota is used by MultiAppTestUtils.verifyDeleteRecords. 302 */ tryAcquireCallQuotaNTimesForWrite(int nTimes)303 private void tryAcquireCallQuotaNTimesForWrite(int nTimes) throws InterruptedException { 304 nTimes = nTimes / mLimitsAdjustmentForTesting; 305 List<Record> testRecord = Arrays.asList(getCompleteStepsRecord()); 306 307 List<Record> insertedRecords = List.of(); 308 for (int i = 0; i < nTimes; i++) { 309 if (i % 3 == 0) { 310 insertedRecords = TestUtils.insertRecords(testRecord); 311 } else if (i % 3 == 1) { 312 List<Record> updateRecords = Arrays.asList(getCompleteStepsRecord()); 313 314 for (int itr = 0; itr < updateRecords.size(); itr++) { 315 updateRecords.set( 316 itr, 317 getUpdatedStepsRecord( 318 updateRecords.get(itr), 319 insertedRecords.get(itr).getMetadata().getId(), 320 insertedRecords.get(itr).getMetadata().getClientRecordId())); 321 } 322 TestUtils.updateRecords(updateRecords); 323 } else { 324 TestUtils.insertRecords(testRecord); 325 // Unmetered rate limiting as Holds data management is true for verify delete 326 // records. 327 TestUtils.verifyDeleteRecords( 328 new DeleteUsingFiltersRequest.Builder() 329 .addRecordType(StepsRecord.class) 330 .build()); 331 } 332 } 333 } 334 readStepsRecordUsingIds(List<Record> recordList)335 private void readStepsRecordUsingIds(List<Record> recordList) throws InterruptedException { 336 ReadRecordsRequestUsingIds.Builder<StepsRecord> request = 337 new ReadRecordsRequestUsingIds.Builder<>(StepsRecord.class); 338 recordList.forEach(v -> request.addId(v.getMetadata().getId())); 339 TestUtils.readRecords(request.build()); 340 } 341 setEnableRateLimiterFlag(boolean flag)342 private void setEnableRateLimiterFlag(boolean flag) throws InterruptedException { 343 if (SdkLevel.isAtLeastU()) { 344 mUiAutomation.adoptShellPermissionIdentity( 345 "android.permission.WRITE_ALLOWLISTED_DEVICE_CONFIG"); 346 } else { 347 mUiAutomation.adoptShellPermissionIdentity("android.permission.WRITE_DEVICE_CONFIG"); 348 } 349 350 DeviceConfig.setProperty( 351 DeviceConfig.NAMESPACE_HEALTH_FITNESS, 352 ENABLE_RATE_LIMITER_FLAG, 353 flag ? "true" : "false", 354 true); 355 mUiAutomation.dropShellPermissionIdentity(); 356 Thread.sleep(100); 357 } 358 } 359