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.health.connect;
18 
19 import static org.hamcrest.CoreMatchers.containsString;
20 
21 import android.Manifest;
22 import android.app.UiAutomation;
23 import android.content.Context;
24 import android.health.connect.ratelimiter.RateLimiter;
25 import android.health.connect.ratelimiter.RateLimiter.QuotaCategory;
26 
27 import androidx.test.platform.app.InstrumentationRegistry;
28 
29 import com.android.modules.utils.testing.ExtendedMockitoRule;
30 import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
31 import com.android.server.healthconnect.TestUtils;
32 
33 import org.junit.After;
34 import org.junit.Before;
35 import org.junit.Rule;
36 import org.junit.Test;
37 import org.junit.rules.ExpectedException;
38 import org.mockito.Mock;
39 import org.mockito.quality.Strictness;
40 
41 import java.time.Duration;
42 import java.time.Instant;
43 
44 public class RateLimiterTest {
45     private static final int UID = 1;
46     private static final boolean IS_IN_FOREGROUND_TRUE = true;
47     private static final boolean IS_IN_FOREGROUND_FALSE = false;
48     private static final int MAX_FOREGROUND_READ_CALL_15M = 2000;
49     private static final int MAX_BACKGROUND_CALL_15M = 1000;
50     private static final Duration WINDOW_15M = Duration.ofMinutes(15);
51     private static final int MEMORY_COST = 20000;
52 
53     private static final UiAutomation UI_AUTOMATION =
54             InstrumentationRegistry.getInstrumentation().getUiAutomation();
55 
56     @Rule public ExpectedException exception = ExpectedException.none();
57 
58     @Rule
59     public final ExtendedMockitoRule mExtendedMockitoRule =
60             new ExtendedMockitoRule.Builder(this).setStrictness(Strictness.LENIENT).build();
61 
62     @Mock Context mContext;
63 
64     @Before
setUp()65     public void setUp() {
66         TestUtils.runWithShellPermissionIdentity(
67                 () -> {
68                     HealthConnectDeviceConfigManager.initializeInstance(mContext);
69                     HealthConnectDeviceConfigManager.getInitialisedInstance()
70                             .updateRateLimiterValues();
71                 },
72                 Manifest.permission.READ_DEVICE_CONFIG);
73         RateLimiter.updateEnableRateLimiterFlag(true);
74     }
75 
76     @After
tearDown()77     public void tearDown() {
78         TestUtils.runWithShellPermissionIdentity(
79                 () -> {
80                     HealthConnectDeviceConfigManager.initializeInstance(mContext);
81                     HealthConnectDeviceConfigManager.getInitialisedInstance()
82                             .updateRateLimiterValues();
83                 },
84                 Manifest.permission.READ_DEVICE_CONFIG);
85     }
86 
87     @Test
testTryAcquireApiCallQuota_invalidQuotaCategory()88     public void testTryAcquireApiCallQuota_invalidQuotaCategory() {
89         RateLimiter.clearCache();
90         @QuotaCategory.Type int quotaCategory = 0;
91         exception.expect(IllegalArgumentException.class);
92         exception.expectMessage("Quota category not defined.");
93         RateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, IS_IN_FOREGROUND_TRUE);
94     }
95 
96     @Test
testTryAcquireApiCallQuota_unmeteredForegroundCalls()97     public void testTryAcquireApiCallQuota_unmeteredForegroundCalls() {
98         RateLimiter.clearCache();
99         @QuotaCategory.Type int quotaCategory = 1;
100         tryAcquireCallQuotaNTimes(
101                 quotaCategory, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M + 1);
102     }
103 
104     @Test
testTryAcquireApiCallQuota_unmeteredBackgroundCalls()105     public void testTryAcquireApiCallQuota_unmeteredBackgroundCalls() {
106         RateLimiter.clearCache();
107         @QuotaCategory.Type int quotaCategory = 1;
108         tryAcquireCallQuotaNTimes(
109                 quotaCategory, IS_IN_FOREGROUND_TRUE, MAX_BACKGROUND_CALL_15M + 1);
110     }
111 
112     @Test
testTryAcquireApiCallQuota_meteredForegroundCallsInLimit()113     public void testTryAcquireApiCallQuota_meteredForegroundCallsInLimit() {
114         RateLimiter.clearCache();
115         @QuotaCategory.Type int quotaCategoryRead = 2;
116         tryAcquireCallQuotaNTimes(
117                 quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M);
118     }
119 
120     @Test
testTryAcquireApiCallQuota_meteredBackgroundCallsInLimit()121     public void testTryAcquireApiCallQuota_meteredBackgroundCallsInLimit() {
122         RateLimiter.clearCache();
123         @QuotaCategory.Type int quotaCategoryWrite = 3;
124         tryAcquireCallQuotaNTimes(
125                 quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, MAX_BACKGROUND_CALL_15M);
126     }
127 
128     @Test
testTryAcquireApiCallQuota_meteredForegroundCallsLimitExceeded()129     public void testTryAcquireApiCallQuota_meteredForegroundCallsLimitExceeded() {
130         RateLimiter.clearCache();
131         @QuotaCategory.Type int quotaCategoryRead = 2;
132         Instant startTime = Instant.now();
133         tryAcquireCallQuotaNTimes(
134                 quotaCategoryRead, IS_IN_FOREGROUND_TRUE, MAX_FOREGROUND_READ_CALL_15M);
135         Instant endTime = Instant.now();
136         int ceilQuotaAcquired =
137                 getCeilQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_FOREGROUND_READ_CALL_15M);
138         exception.expect(HealthConnectException.class);
139         exception.expectMessage(containsString("API call quota exceeded"));
140         tryAcquireCallQuotaNTimes(quotaCategoryRead, IS_IN_FOREGROUND_TRUE, ceilQuotaAcquired);
141     }
142 
143     @Test
testTryAcquireApiCallQuota_meteredBackgroundCallsLimitExceeded()144     public void testTryAcquireApiCallQuota_meteredBackgroundCallsLimitExceeded() {
145         RateLimiter.clearCache();
146         @QuotaCategory.Type int quotaCategoryWrite = 3;
147         Instant startTime = Instant.now();
148         tryAcquireCallQuotaNTimes(
149                 quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, MAX_BACKGROUND_CALL_15M);
150         Instant endTime = Instant.now();
151         int ceilQuotaAcquired =
152                 getCeilQuotaAcquired(startTime, endTime, WINDOW_15M, MAX_BACKGROUND_CALL_15M);
153         exception.expect(HealthConnectException.class);
154         exception.expectMessage(containsString("API call quota exceeded"));
155         tryAcquireCallQuotaNTimes(quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, ceilQuotaAcquired);
156     }
157 
158     @Test
testRecordMemoryRollingQuota_exceedBackgroundLimit()159     public void testRecordMemoryRollingQuota_exceedBackgroundLimit() throws InterruptedException {
160         RateLimiter.clearCache();
161         @QuotaCategory.Type int quotaCategoryWrite = 3;
162         exception.expect(HealthConnectException.class);
163         exception.expectMessage(containsString("API call quota exceeded"));
164         tryAcquireCallQuotaNTimes(
165                 quotaCategoryWrite, IS_IN_FOREGROUND_FALSE, MAX_BACKGROUND_CALL_15M, 40000);
166     }
167 
168     @Test
checkMaxChunkMemoryUsage_LimitExceeded()169     public void checkMaxChunkMemoryUsage_LimitExceeded() {
170         long valueExceeding = 5000001;
171         exception.expect(HealthConnectException.class);
172         exception.expectMessage(
173                 "Records chunk size exceeded the max chunk limit: 5000000, was: 5000001");
174         RateLimiter.checkMaxChunkMemoryUsage(valueExceeding);
175     }
176 
177     @Test
checkMaxChunkMemoryUsage_inLimit()178     public void checkMaxChunkMemoryUsage_inLimit() {
179         long value = 5000000;
180         RateLimiter.checkMaxChunkMemoryUsage(value);
181     }
182 
183     @Test
checkMaxRecordMemoryUsage_LimitExceeded()184     public void checkMaxRecordMemoryUsage_LimitExceeded() {
185         long valueExceeding = 1000001;
186         exception.expect(HealthConnectException.class);
187         exception.expectMessage(
188                 "Record size exceeded the single record size limit: 1000000, was: 1000001");
189         RateLimiter.checkMaxRecordMemoryUsage(valueExceeding);
190     }
191 
192     @Test
checkMaxRecordMemoryUsage_inLimit()193     public void checkMaxRecordMemoryUsage_inLimit() {
194         long value = 1000000;
195         RateLimiter.checkMaxRecordMemoryUsage(value);
196     }
197 
getCeilQuotaAcquired( Instant startTime, Instant endTime, Duration window, int maxQuota)198     private int getCeilQuotaAcquired(
199             Instant startTime, Instant endTime, Duration window, int maxQuota) {
200         Duration timeSpent = Duration.between(startTime, endTime);
201         float accumulated = timeSpent.toMillis() * ((float) maxQuota / (float) window.toMillis());
202         return accumulated > (int) accumulated
203                 ? (int) Math.ceil(accumulated)
204                 : (int) accumulated + 1;
205     }
206 
tryAcquireCallQuotaNTimes( @uotaCategory.Type int quotaCategory, boolean isInForeground, int nTimes)207     private void tryAcquireCallQuotaNTimes(
208             @QuotaCategory.Type int quotaCategory, boolean isInForeground, int nTimes) {
209 
210         if (quotaCategory == QuotaCategory.QUOTA_CATEGORY_WRITE) {
211             for (int i = 0; i < nTimes; i++) {
212                 RateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, isInForeground, MEMORY_COST);
213             }
214         } else {
215             for (int i = 0; i < nTimes; i++) {
216                 RateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, isInForeground);
217             }
218         }
219     }
220 
tryAcquireCallQuotaNTimes( @uotaCategory.Type int quotaCategory, boolean isInForeground, int nTimes, int memoryCost)221     private void tryAcquireCallQuotaNTimes(
222             @QuotaCategory.Type int quotaCategory,
223             boolean isInForeground,
224             int nTimes,
225             int memoryCost) {
226         for (int i = 0; i < nTimes; i++) {
227             RateLimiter.tryAcquireApiCallQuota(UID, quotaCategory, isInForeground, memoryCost);
228         }
229     }
230 }
231