1 /*
2  * Copyright (C) 2018 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 com.android.server.connectivity;
18 
19 import static android.content.Intent.ACTION_CONFIGURATION_CHANGED;
20 import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
21 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
22 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
23 import static android.net.NetworkPolicy.LIMIT_DISABLED;
24 import static android.net.NetworkPolicy.SNOOZE_NEVER;
25 import static android.net.NetworkPolicy.WARNING_DISABLED;
26 import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
27 
28 import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
29 import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
30 import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
31 
32 import static org.junit.Assert.assertNotNull;
33 import static org.mockito.ArgumentMatchers.any;
34 import static org.mockito.ArgumentMatchers.anyInt;
35 import static org.mockito.ArgumentMatchers.argThat;
36 import static org.mockito.ArgumentMatchers.eq;
37 import static org.mockito.Mockito.doCallRealMethod;
38 import static org.mockito.Mockito.doReturn;
39 import static org.mockito.Mockito.mock;
40 import static org.mockito.Mockito.times;
41 import static org.mockito.Mockito.verify;
42 import static org.mockito.Mockito.when;
43 
44 import android.app.usage.NetworkStats;
45 import android.app.usage.NetworkStatsManager;
46 import android.content.BroadcastReceiver;
47 import android.content.Context;
48 import android.content.Intent;
49 import android.content.pm.ApplicationInfo;
50 import android.content.res.Resources;
51 import android.net.ConnectivityManager;
52 import android.net.EthernetNetworkSpecifier;
53 import android.net.Network;
54 import android.net.NetworkCapabilities;
55 import android.net.NetworkPolicy;
56 import android.net.NetworkPolicyManager;
57 import android.net.NetworkTemplate;
58 import android.net.TelephonyNetworkSpecifier;
59 import android.os.Build;
60 import android.os.Handler;
61 import android.os.UserHandle;
62 import android.provider.Settings;
63 import android.telephony.TelephonyManager;
64 import android.test.mock.MockContentResolver;
65 import android.util.DataUnit;
66 import android.util.Range;
67 import android.util.RecurrenceRule;
68 
69 import androidx.test.filters.SmallTest;
70 
71 import com.android.internal.R;
72 import com.android.internal.util.test.FakeSettingsProvider;
73 import com.android.server.LocalServices;
74 import com.android.server.net.NetworkPolicyManagerInternal;
75 import com.android.testutils.DevSdkIgnoreRule;
76 import com.android.testutils.DevSdkIgnoreRunner;
77 
78 import org.junit.After;
79 import org.junit.Before;
80 import org.junit.Test;
81 import org.junit.runner.RunWith;
82 import org.mockito.ArgumentCaptor;
83 import org.mockito.Mock;
84 import org.mockito.Mockito;
85 import org.mockito.MockitoAnnotations;
86 
87 import java.time.Clock;
88 import java.time.Instant;
89 import java.time.Period;
90 import java.time.ZoneId;
91 import java.time.ZonedDateTime;
92 import java.time.temporal.ChronoUnit;
93 import java.util.Set;
94 
95 @RunWith(DevSdkIgnoreRunner.class)
96 @SmallTest
97 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
98 public class MultipathPolicyTrackerTest {
99     private static final Network TEST_NETWORK = new Network(123);
100     private static final int POLICY_SNOOZED = -100;
101     private static final String TEST_IMSI1 = "TEST_IMSI1";
102 
103     @Mock private Context mContext;
104     @Mock private Context mUserAllContext;
105     @Mock private Resources mResources;
106     @Mock private Handler mHandler;
107     @Mock private MultipathPolicyTracker.Dependencies mDeps;
108     @Mock private Clock mClock;
109     @Mock private ConnectivityManager mCM;
110     @Mock private NetworkPolicyManager mNPM;
111     @Mock private NetworkStatsManager mStatsManager;
112     @Mock private NetworkPolicyManagerInternal mNPMI;
113     @Mock private TelephonyManager mTelephonyManager;
114     private MockContentResolver mContentResolver;
115 
116     private ArgumentCaptor<BroadcastReceiver> mConfigChangeReceiverCaptor;
117 
118     private MultipathPolicyTracker mTracker;
119 
120     private Clock mPreviousRecurrenceRuleClock;
121     private boolean mRecurrenceRuleClockMocked;
122 
mockService(String serviceName, Class<T> serviceClass, T service)123     private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
124         doReturn(serviceName).when(mContext).getSystemServiceName(serviceClass);
125         doReturn(service).when(mContext).getSystemService(serviceName);
126         if (mContext.getSystemService(serviceClass) == null) {
127             // Test is using mockito-extended
128             doCallRealMethod().when(mContext).getSystemService(serviceClass);
129         }
130     }
131 
132     @Before
setUp()133     public void setUp() {
134         MockitoAnnotations.initMocks(this);
135 
136         mPreviousRecurrenceRuleClock = RecurrenceRule.sClock;
137         RecurrenceRule.sClock = mClock;
138         mRecurrenceRuleClockMocked = true;
139 
140         mConfigChangeReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class);
141 
142         when(mContext.getResources()).thenReturn(mResources);
143         when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
144         // Mock user id to all users that Context#registerReceiver will register with all users too.
145         doReturn(UserHandle.ALL.getIdentifier()).when(mUserAllContext).getUserId();
146         when(mContext.createContextAsUser(eq(UserHandle.ALL), anyInt()))
147                 .thenReturn(mUserAllContext);
148         when(mUserAllContext.registerReceiver(mConfigChangeReceiverCaptor.capture(),
149                 argThat(f -> f.hasAction(ACTION_CONFIGURATION_CHANGED)), any(), any()))
150                 .thenReturn(null);
151 
152         when(mDeps.getClock()).thenReturn(mClock);
153 
154         when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
155         when(mTelephonyManager.getSubscriberId()).thenReturn(TEST_IMSI1);
156 
157         mContentResolver = Mockito.spy(new MockContentResolver(mContext));
158         mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
159         Settings.Global.clearProviderForTest();
160         when(mContext.getContentResolver()).thenReturn(mContentResolver);
161 
162         mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, mCM);
163         mockService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, mNPM);
164         mockService(Context.NETWORK_STATS_SERVICE, NetworkStatsManager.class, mStatsManager);
165         mockService(Context.TELEPHONY_SERVICE, TelephonyManager.class, mTelephonyManager);
166 
167         LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class);
168         LocalServices.addService(NetworkPolicyManagerInternal.class, mNPMI);
169 
170         mTracker = new MultipathPolicyTracker(mContext, mHandler, mDeps);
171     }
172 
173     @After
tearDown()174     public void tearDown() {
175         // Avoid setting static clock to null (which should normally not be the case)
176         // if MockitoAnnotations.initMocks threw an exception
177         if (mRecurrenceRuleClockMocked) {
178             RecurrenceRule.sClock = mPreviousRecurrenceRuleClock;
179         }
180         mRecurrenceRuleClockMocked = false;
181     }
182 
setDefaultQuotaGlobalSetting(long setting)183     private void setDefaultQuotaGlobalSetting(long setting) {
184         Settings.Global.putInt(mContentResolver, NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES,
185                 (int) setting);
186     }
187 
prepareGetMultipathPreferenceTest( long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit, long defaultGlobalSetting, long defaultResSetting, boolean roaming)188     private void prepareGetMultipathPreferenceTest(
189             long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit,
190             long defaultGlobalSetting, long defaultResSetting, boolean roaming) {
191 
192         // TODO: tests should not use ZoneId.systemDefault() once code handles TZ correctly.
193         final ZonedDateTime now = ZonedDateTime.ofInstant(
194                 Instant.parse("2017-04-02T10:11:12Z"), ZoneId.systemDefault());
195         final ZonedDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS);
196         when(mClock.millis()).thenReturn(now.toInstant().toEpochMilli());
197         when(mClock.instant()).thenReturn(now.toInstant());
198         when(mClock.getZone()).thenReturn(ZoneId.systemDefault());
199 
200         // Setup plan quota
201         when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
202                 .thenReturn(subscriptionQuota);
203 
204         // Prepare stats to be mocked.
205         final NetworkStats.Bucket mockedStatsBucket = mock(NetworkStats.Bucket.class);
206         when(mockedStatsBucket.getTxBytes()).thenReturn(usedBytesToday / 3);
207         when(mockedStatsBucket.getRxBytes()).thenReturn(usedBytesToday - usedBytesToday / 3);
208 
209         // Setup user policy warning / limit
210         if (policyWarning != WARNING_DISABLED || policyLimit != LIMIT_DISABLED) {
211             final Instant recurrenceStart = Instant.parse("2017-04-01T00:00:00Z");
212             final RecurrenceRule recurrenceRule = new RecurrenceRule(
213                     ZonedDateTime.ofInstant(
214                             recurrenceStart,
215                             ZoneId.systemDefault()),
216                     null /* end */,
217                     Period.ofMonths(1));
218             final boolean snoozeWarning = policyWarning == POLICY_SNOOZED;
219             final boolean snoozeLimit = policyLimit == POLICY_SNOOZED;
220             when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[] {
221                     new NetworkPolicy(
222                             new NetworkTemplate.Builder(NetworkTemplate.MATCH_MOBILE)
223                                     .setSubscriberIds(Set.of(TEST_IMSI1))
224                                     .setMeteredness(android.net.NetworkStats.METERED_YES).build(),
225                             recurrenceRule,
226                             snoozeWarning ? 0 : policyWarning,
227                             snoozeLimit ? 0 : policyLimit,
228                             snoozeWarning ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER,
229                             snoozeLimit ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER,
230                             SNOOZE_NEVER,
231                             true /* metered */,
232                             false /* inferred */)
233             });
234 
235             // Mock stats for this month.
236             final Range<ZonedDateTime> cycleOfTheMonth = recurrenceRule.cycleIterator().next();
237             when(mStatsManager.querySummaryForDevice(any(),
238                     eq(cycleOfTheMonth.getLower().toInstant().toEpochMilli()),
239                     eq(cycleOfTheMonth.getUpper().toInstant().toEpochMilli())))
240                     .thenReturn(mockedStatsBucket);
241         } else {
242             when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[0]);
243         }
244 
245         // Setup default quota in settings and resources
246         if (defaultGlobalSetting > 0) {
247             setDefaultQuotaGlobalSetting(defaultGlobalSetting);
248         }
249         when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
250                 .thenReturn((int) defaultResSetting);
251 
252         // Mock stats for today.
253         when(mStatsManager.querySummaryForDevice(any(),
254                 eq(startOfDay.toInstant().toEpochMilli()),
255                 eq(now.toInstant().toEpochMilli()))).thenReturn(mockedStatsBucket);
256 
257         ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback =
258                 ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
259         mTracker.start();
260         verify(mCM).registerNetworkCallback(any(), networkCallback.capture(), any());
261 
262         // Simulate callback after capability changes
263         NetworkCapabilities capabilities = new NetworkCapabilities()
264                 .addCapability(NET_CAPABILITY_INTERNET)
265                 .addTransportType(TRANSPORT_CELLULAR)
266                 .setNetworkSpecifier(new EthernetNetworkSpecifier("eth234"));
267         if (!roaming) {
268             capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING);
269         }
270         networkCallback.getValue().onCapabilitiesChanged(
271                 TEST_NETWORK,
272                 capabilities);
273 
274         // make sure it also works with the new introduced  TelephonyNetworkSpecifier
275         capabilities = new NetworkCapabilities()
276                 .addCapability(NET_CAPABILITY_INTERNET)
277                 .addTransportType(TRANSPORT_CELLULAR)
278                 .setNetworkSpecifier(new TelephonyNetworkSpecifier.Builder()
279                         .setSubscriptionId(234).build());
280         if (!roaming) {
281             capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING);
282         }
283         networkCallback.getValue().onCapabilitiesChanged(
284                 TEST_NETWORK,
285                 capabilities);
286     }
287 
288     @Test
testGetMultipathPreference_SubscriptionQuota()289     public void testGetMultipathPreference_SubscriptionQuota() {
290         prepareGetMultipathPreferenceTest(
291                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
292                 DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
293                 DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
294                 LIMIT_DISABLED,
295                 DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
296                 2_500_000 /* defaultResSetting */,
297                 false /* roaming */);
298 
299         verify(mStatsManager, times(1)).registerUsageCallback(
300                 any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
301     }
302 
303     @Test
testGetMultipathPreference_UserWarningQuota()304     public void testGetMultipathPreference_UserWarningQuota() {
305         prepareGetMultipathPreferenceTest(
306                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
307                 OPPORTUNISTIC_QUOTA_UNKNOWN,
308                 // Remaining days are 29 days from Apr. 2nd to May 1st.
309                 // Set limit so that 15MB * remaining days will be 5% of the remaining limit,
310                 // so it will be 15 * 29 / 0.05 + used bytes.
311                 DataUnit.MEGABYTES.toBytes(15 * 29 * 20 + 7) /* policyWarning */,
312                 LIMIT_DISABLED,
313                 DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
314                 2_500_000 /* defaultResSetting */,
315                 false /* roaming */);
316 
317         // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
318         verify(mStatsManager, times(1)).registerUsageCallback(
319                 any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
320     }
321 
322     @Test
testGetMultipathPreference_SnoozedWarningQuota()323     public void testGetMultipathPreference_SnoozedWarningQuota() {
324         prepareGetMultipathPreferenceTest(
325                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
326                 OPPORTUNISTIC_QUOTA_UNKNOWN,
327                 POLICY_SNOOZED /* policyWarning */,
328                 // Remaining days are 29 days from Apr. 2nd to May 1st.
329                 // Set limit so that 15MB * remaining days will be 5% of the remaining limit,
330                 // so it will be 15 * 29 / 0.05 + used bytes.
331                 DataUnit.MEGABYTES.toBytes(15 * 29 * 20 + 7) /* policyLimit */,
332                 DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
333                 2_500_000 /* defaultResSetting */,
334                 false /* roaming */);
335 
336         // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
337         verify(mStatsManager, times(1)).registerUsageCallback(
338                 any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
339     }
340 
341     @Test
testGetMultipathPreference_SnoozedBothQuota()342     public void testGetMultipathPreference_SnoozedBothQuota() {
343         prepareGetMultipathPreferenceTest(
344                 DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
345                 OPPORTUNISTIC_QUOTA_UNKNOWN,
346                 // 29 days from Apr. 2nd to May 1st
347                 POLICY_SNOOZED /* policyWarning */,
348                 POLICY_SNOOZED /* policyLimit */,
349                 DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
350                 2_500_000 /* defaultResSetting */,
351                 false /* roaming */);
352 
353         // Default global setting should be used: 12 - 7 = 5
354         verify(mStatsManager, times(1)).registerUsageCallback(
355                 any(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
356     }
357 
358     @Test
testGetMultipathPreference_SettingChanged()359     public void testGetMultipathPreference_SettingChanged() {
360         prepareGetMultipathPreferenceTest(
361                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
362                 OPPORTUNISTIC_QUOTA_UNKNOWN,
363                 WARNING_DISABLED,
364                 LIMIT_DISABLED,
365                 -1 /* defaultGlobalSetting */,
366                 DataUnit.MEGABYTES.toBytes(10) /* defaultResSetting */,
367                 false /* roaming */);
368 
369         verify(mStatsManager, times(1)).registerUsageCallback(
370                 any(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
371 
372         // Update setting
373         setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14));
374         mTracker.mSettingsObserver.onChange(
375                 false, Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES));
376 
377         // Callback must have been re-registered with new setting
378         verify(mStatsManager, times(1)).unregisterUsageCallback(any());
379         verify(mStatsManager, times(1)).registerUsageCallback(
380                 any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
381     }
382 
383     @Test
testGetMultipathPreference_ResourceChanged()384     public void testGetMultipathPreference_ResourceChanged() {
385         prepareGetMultipathPreferenceTest(
386                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
387                 OPPORTUNISTIC_QUOTA_UNKNOWN,
388                 WARNING_DISABLED,
389                 LIMIT_DISABLED,
390                 -1 /* defaultGlobalSetting */,
391                 DataUnit.MEGABYTES.toBytes(14) /* defaultResSetting */,
392                 false /* roaming */);
393 
394         verify(mStatsManager, times(1)).registerUsageCallback(
395                 any(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
396 
397         when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
398                 .thenReturn((int) DataUnit.MEGABYTES.toBytes(16));
399 
400         final BroadcastReceiver configChangeReceiver = mConfigChangeReceiverCaptor.getValue();
401         assertNotNull(configChangeReceiver);
402         configChangeReceiver.onReceive(mContext, new Intent());
403 
404         // Uses the new setting (16 - 2 = 14MB)
405         verify(mStatsManager, times(1)).registerUsageCallback(
406                 any(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
407     }
408 
409     @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
410     @Test
testOnThresholdReached()411     public void testOnThresholdReached() {
412         prepareGetMultipathPreferenceTest(
413                 DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
414                 DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
415                 DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
416                 LIMIT_DISABLED,
417                 DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
418                 2_500_000 /* defaultResSetting */,
419                 false /* roaming */);
420 
421         final ArgumentCaptor<NetworkStatsManager.UsageCallback> usageCallbackCaptor =
422                 ArgumentCaptor.forClass(NetworkStatsManager.UsageCallback.class);
423         final ArgumentCaptor<NetworkTemplate> networkTemplateCaptor =
424                 ArgumentCaptor.forClass(NetworkTemplate.class);
425         // Verify the callback is registered with quota - used = 14 - 2 = 12MB.
426         verify(mStatsManager, times(1)).registerUsageCallback(
427                 networkTemplateCaptor.capture(), eq(DataUnit.MEGABYTES.toBytes(12)), any(),
428                 usageCallbackCaptor.capture());
429 
430         // Capture arguments for later use.
431         final NetworkStatsManager.UsageCallback usageCallback = usageCallbackCaptor.getValue();
432         final NetworkTemplate template = networkTemplateCaptor.getValue();
433         assertNotNull(usageCallback);
434         assertNotNull(template);
435 
436         // Decrease quota from 14 to 11, and trigger the event.
437         // TODO: Mock daily and monthly used bytes instead of changing subscription to simulate
438         //  remaining quota changed.
439         when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
440                 .thenReturn(DataUnit.MEGABYTES.toBytes(11));
441         usageCallback.onThresholdReached(template);
442 
443         // Callback must have been re-registered with new remaining quota = 11 - 2 = 9MB.
444         verify(mStatsManager, times(1))
445                 .unregisterUsageCallback(eq(usageCallback));
446         verify(mStatsManager, times(1)).registerUsageCallback(
447                 eq(template), eq(DataUnit.MEGABYTES.toBytes(9)), any(), eq(usageCallback));
448     }
449 }
450