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
17package com.android.server.connectivity;
18
19import static android.content.Intent.ACTION_CONFIGURATION_CHANGED;
20import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
21import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
22import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
23import static android.net.NetworkPolicy.LIMIT_DISABLED;
24import static android.net.NetworkPolicy.SNOOZE_NEVER;
25import static android.net.NetworkPolicy.WARNING_DISABLED;
26import static android.provider.Settings.Global.NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES;
27
28import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
29import static com.android.server.net.NetworkPolicyManagerService.OPPORTUNISTIC_QUOTA_UNKNOWN;
30
31import static junit.framework.TestCase.assertEquals;
32import static junit.framework.TestCase.assertNotNull;
33
34import static org.mockito.ArgumentMatchers.any;
35import static org.mockito.ArgumentMatchers.anyInt;
36import static org.mockito.ArgumentMatchers.argThat;
37import static org.mockito.ArgumentMatchers.eq;
38import static org.mockito.Mockito.times;
39import static org.mockito.Mockito.verify;
40import static org.mockito.Mockito.when;
41
42import android.app.usage.NetworkStatsManager;
43import android.content.BroadcastReceiver;
44import android.content.Context;
45import android.content.Intent;
46import android.content.pm.ApplicationInfo;
47import android.content.res.Resources;
48import android.net.ConnectivityManager;
49import android.net.Network;
50import android.net.NetworkCapabilities;
51import android.net.NetworkPolicy;
52import android.net.NetworkPolicyManager;
53import android.net.NetworkTemplate;
54import android.net.StringNetworkSpecifier;
55import android.os.Handler;
56import android.provider.Settings;
57import android.support.test.filters.SmallTest;
58import android.support.test.runner.AndroidJUnit4;
59import android.telephony.TelephonyManager;
60import android.test.mock.MockContentResolver;
61import android.util.DataUnit;
62import android.util.RecurrenceRule;
63
64import com.android.internal.R;
65import com.android.internal.util.test.FakeSettingsProvider;
66import com.android.server.LocalServices;
67import com.android.server.net.NetworkPolicyManagerInternal;
68import com.android.server.net.NetworkStatsManagerInternal;
69
70import org.junit.After;
71import org.junit.Before;
72import org.junit.Test;
73import org.junit.runner.RunWith;
74import org.mockito.ArgumentCaptor;
75import org.mockito.Mock;
76import org.mockito.Mockito;
77import org.mockito.MockitoAnnotations;
78
79import java.time.Clock;
80import java.time.Instant;
81import java.time.Period;
82import java.time.ZoneId;
83import java.time.ZonedDateTime;
84import java.time.temporal.ChronoUnit;
85
86@RunWith(AndroidJUnit4.class)
87@SmallTest
88public class MultipathPolicyTrackerTest {
89    private static final Network TEST_NETWORK = new Network(123);
90    private static final int POLICY_SNOOZED = -100;
91
92    @Mock private Context mContext;
93    @Mock private Resources mResources;
94    @Mock private Handler mHandler;
95    @Mock private MultipathPolicyTracker.Dependencies mDeps;
96    @Mock private Clock mClock;
97    @Mock private ConnectivityManager mCM;
98    @Mock private NetworkPolicyManager mNPM;
99    @Mock private NetworkStatsManager mStatsManager;
100    @Mock private NetworkPolicyManagerInternal mNPMI;
101    @Mock private NetworkStatsManagerInternal mNetworkStatsManagerInternal;
102    @Mock private TelephonyManager mTelephonyManager;
103    private MockContentResolver mContentResolver;
104
105    private ArgumentCaptor<BroadcastReceiver> mConfigChangeReceiverCaptor;
106
107    private MultipathPolicyTracker mTracker;
108
109    private Clock mPreviousRecurrenceRuleClock;
110    private boolean mRecurrenceRuleClockMocked;
111
112    private <T> void mockService(String serviceName, Class<T> serviceClass, T service) {
113        when(mContext.getSystemServiceName(serviceClass)).thenReturn(serviceName);
114        when(mContext.getSystemService(serviceName)).thenReturn(service);
115    }
116
117    @Before
118    public void setUp() {
119        MockitoAnnotations.initMocks(this);
120
121        mPreviousRecurrenceRuleClock = RecurrenceRule.sClock;
122        RecurrenceRule.sClock = mClock;
123        mRecurrenceRuleClockMocked = true;
124
125        mConfigChangeReceiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class);
126
127        when(mContext.getResources()).thenReturn(mResources);
128        when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
129        when(mContext.registerReceiverAsUser(mConfigChangeReceiverCaptor.capture(),
130                any(), argThat(f -> f.hasAction(ACTION_CONFIGURATION_CHANGED)), any(), any()))
131                .thenReturn(null);
132
133        when(mDeps.getClock()).thenReturn(mClock);
134
135        when(mTelephonyManager.createForSubscriptionId(anyInt())).thenReturn(mTelephonyManager);
136
137        mContentResolver = Mockito.spy(new MockContentResolver(mContext));
138        mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider());
139        Settings.Global.clearProviderForTest();
140        when(mContext.getContentResolver()).thenReturn(mContentResolver);
141
142        mockService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class, mCM);
143        mockService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, mNPM);
144        mockService(Context.NETWORK_STATS_SERVICE, NetworkStatsManager.class, mStatsManager);
145        mockService(Context.TELEPHONY_SERVICE, TelephonyManager.class, mTelephonyManager);
146
147        LocalServices.removeServiceForTest(NetworkPolicyManagerInternal.class);
148        LocalServices.addService(NetworkPolicyManagerInternal.class, mNPMI);
149
150        LocalServices.removeServiceForTest(NetworkStatsManagerInternal.class);
151        LocalServices.addService(NetworkStatsManagerInternal.class, mNetworkStatsManagerInternal);
152
153        mTracker = new MultipathPolicyTracker(mContext, mHandler, mDeps);
154    }
155
156    @After
157    public void tearDown() {
158        // Avoid setting static clock to null (which should normally not be the case)
159        // if MockitoAnnotations.initMocks threw an exception
160        if (mRecurrenceRuleClockMocked) {
161            RecurrenceRule.sClock = mPreviousRecurrenceRuleClock;
162        }
163        mRecurrenceRuleClockMocked = false;
164    }
165
166    private void setDefaultQuotaGlobalSetting(long setting) {
167        Settings.Global.putInt(mContentResolver, NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES,
168                (int) setting);
169    }
170
171    private void testGetMultipathPreference(
172            long usedBytesToday, long subscriptionQuota, long policyWarning, long policyLimit,
173            long defaultGlobalSetting, long defaultResSetting, boolean roaming) {
174
175        // TODO: tests should not use ZoneId.systemDefault() once code handles TZ correctly.
176        final ZonedDateTime now = ZonedDateTime.ofInstant(
177                Instant.parse("2017-04-02T10:11:12Z"), ZoneId.systemDefault());
178        final ZonedDateTime startOfDay = now.truncatedTo(ChronoUnit.DAYS);
179        when(mClock.millis()).thenReturn(now.toInstant().toEpochMilli());
180        when(mClock.instant()).thenReturn(now.toInstant());
181        when(mClock.getZone()).thenReturn(ZoneId.systemDefault());
182
183        // Setup plan quota
184        when(mNPMI.getSubscriptionOpportunisticQuota(TEST_NETWORK, QUOTA_TYPE_MULTIPATH))
185                .thenReturn(subscriptionQuota);
186
187        // Setup user policy warning / limit
188        if (policyWarning != WARNING_DISABLED || policyLimit != LIMIT_DISABLED) {
189            final Instant recurrenceStart = Instant.parse("2017-04-01T00:00:00Z");
190            final RecurrenceRule recurrenceRule = new RecurrenceRule(
191                    ZonedDateTime.ofInstant(
192                            recurrenceStart,
193                            ZoneId.systemDefault()),
194                    null /* end */,
195                    Period.ofMonths(1));
196            final boolean snoozeWarning = policyWarning == POLICY_SNOOZED;
197            final boolean snoozeLimit = policyLimit == POLICY_SNOOZED;
198            when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[] {
199                    new NetworkPolicy(
200                            NetworkTemplate.buildTemplateMobileWildcard(),
201                            recurrenceRule,
202                            snoozeWarning ? 0 : policyWarning,
203                            snoozeLimit ? 0 : policyLimit,
204                            snoozeWarning ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER,
205                            snoozeLimit ? recurrenceStart.toEpochMilli() + 1 : SNOOZE_NEVER,
206                            SNOOZE_NEVER,
207                            true /* metered */,
208                            false /* inferred */)
209            });
210        } else {
211            when(mNPM.getNetworkPolicies()).thenReturn(new NetworkPolicy[0]);
212        }
213
214        // Setup default quota in settings and resources
215        if (defaultGlobalSetting > 0) {
216            setDefaultQuotaGlobalSetting(defaultGlobalSetting);
217        }
218        when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
219                .thenReturn((int) defaultResSetting);
220
221        when(mNetworkStatsManagerInternal.getNetworkTotalBytes(
222                any(),
223                eq(startOfDay.toInstant().toEpochMilli()),
224                eq(now.toInstant().toEpochMilli()))).thenReturn(usedBytesToday);
225
226        ArgumentCaptor<ConnectivityManager.NetworkCallback> networkCallback =
227                ArgumentCaptor.forClass(ConnectivityManager.NetworkCallback.class);
228        mTracker.start();
229        verify(mCM).registerNetworkCallback(any(), networkCallback.capture(), any());
230
231        // Simulate callback after capability changes
232        final NetworkCapabilities capabilities = new NetworkCapabilities()
233                .addCapability(NET_CAPABILITY_INTERNET)
234                .addTransportType(TRANSPORT_CELLULAR)
235                .setNetworkSpecifier(new StringNetworkSpecifier("234"));
236        if (!roaming) {
237            capabilities.addCapability(NET_CAPABILITY_NOT_ROAMING);
238        }
239        networkCallback.getValue().onCapabilitiesChanged(
240                TEST_NETWORK,
241                capabilities);
242    }
243
244    @Test
245    public void testGetMultipathPreference_SubscriptionQuota() {
246        testGetMultipathPreference(
247                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
248                DataUnit.MEGABYTES.toBytes(14) /* subscriptionQuota */,
249                DataUnit.MEGABYTES.toBytes(100) /* policyWarning */,
250                LIMIT_DISABLED,
251                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
252                2_500_000 /* defaultResSetting */,
253                false /* roaming */);
254
255        verify(mStatsManager, times(1)).registerUsageCallback(
256                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
257    }
258
259    @Test
260    public void testGetMultipathPreference_UserWarningQuota() {
261        testGetMultipathPreference(
262                DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
263                OPPORTUNISTIC_QUOTA_UNKNOWN,
264                // 29 days from Apr. 2nd to May 1st
265                DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyWarning */,
266                LIMIT_DISABLED,
267                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
268                2_500_000 /* defaultResSetting */,
269                false /* roaming */);
270
271        // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
272        verify(mStatsManager, times(1)).registerUsageCallback(
273                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
274    }
275
276    @Test
277    public void testGetMultipathPreference_SnoozedWarningQuota() {
278        testGetMultipathPreference(
279                DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
280                OPPORTUNISTIC_QUOTA_UNKNOWN,
281                // 29 days from Apr. 2nd to May 1st
282                POLICY_SNOOZED /* policyWarning */,
283                DataUnit.MEGABYTES.toBytes(15 * 29 * 20) /* policyLimit */,
284                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
285                2_500_000 /* defaultResSetting */,
286                false /* roaming */);
287
288        // Daily budget should be 15MB (5% of daily quota), 7MB used today: callback set for 8MB
289        verify(mStatsManager, times(1)).registerUsageCallback(
290                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
291    }
292
293    @Test
294    public void testGetMultipathPreference_SnoozedBothQuota() {
295        testGetMultipathPreference(
296                DataUnit.MEGABYTES.toBytes(7) /* usedBytesToday */,
297                OPPORTUNISTIC_QUOTA_UNKNOWN,
298                // 29 days from Apr. 2nd to May 1st
299                POLICY_SNOOZED /* policyWarning */,
300                POLICY_SNOOZED /* policyLimit */,
301                DataUnit.MEGABYTES.toBytes(12) /* defaultGlobalSetting */,
302                2_500_000 /* defaultResSetting */,
303                false /* roaming */);
304
305        // Default global setting should be used: 12 - 7 = 5
306        verify(mStatsManager, times(1)).registerUsageCallback(
307                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(5)), any(), any());
308    }
309
310    @Test
311    public void testGetMultipathPreference_SettingChanged() {
312        testGetMultipathPreference(
313                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
314                OPPORTUNISTIC_QUOTA_UNKNOWN,
315                WARNING_DISABLED,
316                LIMIT_DISABLED,
317                -1 /* defaultGlobalSetting */,
318                DataUnit.MEGABYTES.toBytes(10) /* defaultResSetting */,
319                false /* roaming */);
320
321        verify(mStatsManager, times(1)).registerUsageCallback(
322                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(8)), any(), any());
323
324        // Update setting
325        setDefaultQuotaGlobalSetting(DataUnit.MEGABYTES.toBytes(14));
326        mTracker.mSettingsObserver.onChange(
327                false, Settings.Global.getUriFor(NETWORK_DEFAULT_DAILY_MULTIPATH_QUOTA_BYTES));
328
329        // Callback must have been re-registered with new setting
330        verify(mStatsManager, times(1)).unregisterUsageCallback(any());
331        verify(mStatsManager, times(1)).registerUsageCallback(
332                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
333    }
334
335    @Test
336    public void testGetMultipathPreference_ResourceChanged() {
337        testGetMultipathPreference(
338                DataUnit.MEGABYTES.toBytes(2) /* usedBytesToday */,
339                OPPORTUNISTIC_QUOTA_UNKNOWN,
340                WARNING_DISABLED,
341                LIMIT_DISABLED,
342                -1 /* defaultGlobalSetting */,
343                DataUnit.MEGABYTES.toBytes(14) /* defaultResSetting */,
344                false /* roaming */);
345
346        verify(mStatsManager, times(1)).registerUsageCallback(
347                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(12)), any(), any());
348
349        when(mResources.getInteger(R.integer.config_networkDefaultDailyMultipathQuotaBytes))
350                .thenReturn((int) DataUnit.MEGABYTES.toBytes(16));
351
352        final BroadcastReceiver configChangeReceiver = mConfigChangeReceiverCaptor.getValue();
353        assertNotNull(configChangeReceiver);
354        configChangeReceiver.onReceive(mContext, new Intent());
355
356        // Uses the new setting (16 - 2 = 14MB)
357        verify(mStatsManager, times(1)).registerUsageCallback(
358                any(), anyInt(), eq(DataUnit.MEGABYTES.toBytes(14)), any(), any());
359    }
360}
361