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