1 /*
2  * Copyright (C) 2019 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.cts.netpolicy.hostside;
18 
19 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_DISABLED;
20 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
21 import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
22 import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
23 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
24 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
25 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_METERED;
26 import static android.net.wifi.WifiConfiguration.METERED_OVERRIDE_NONE;
27 
28 import static com.android.compatibility.common.util.SystemUtil.runShellCommandOrThrow;
29 import static com.android.cts.netpolicy.hostside.AbstractRestrictBackgroundNetworkTestCase.TAG;
30 
31 import static org.junit.Assert.assertEquals;
32 import static org.junit.Assert.assertNotEquals;
33 import static org.junit.Assert.assertNotNull;
34 import static org.junit.Assert.assertTrue;
35 import static org.junit.Assert.fail;
36 
37 import android.app.ActivityManager;
38 import android.app.Instrumentation;
39 import android.app.UiAutomation;
40 import android.content.Context;
41 import android.content.pm.PackageManager;
42 import android.location.LocationManager;
43 import android.net.ConnectivityManager;
44 import android.net.ConnectivityManager.NetworkCallback;
45 import android.net.Network;
46 import android.net.NetworkCapabilities;
47 import android.net.NetworkPolicyManager;
48 import android.net.wifi.WifiConfiguration;
49 import android.net.wifi.WifiManager;
50 import android.net.wifi.WifiManager.ActionListener;
51 import android.os.PersistableBundle;
52 import android.os.Process;
53 import android.os.UserHandle;
54 import android.telephony.CarrierConfigManager;
55 import android.telephony.SubscriptionManager;
56 import android.telephony.data.ApnSetting;
57 import android.util.Log;
58 
59 import androidx.test.platform.app.InstrumentationRegistry;
60 import androidx.test.uiautomator.UiDevice;
61 
62 import com.android.compatibility.common.util.AppStandbyUtils;
63 import com.android.compatibility.common.util.BatteryUtils;
64 import com.android.compatibility.common.util.PollingCheck;
65 import com.android.compatibility.common.util.ShellIdentityUtils;
66 import com.android.compatibility.common.util.ThrowingRunnable;
67 
68 import java.util.ArrayList;
69 import java.util.Arrays;
70 import java.util.List;
71 import java.util.concurrent.BlockingQueue;
72 import java.util.concurrent.CountDownLatch;
73 import java.util.concurrent.LinkedBlockingQueue;
74 import java.util.concurrent.TimeUnit;
75 
76 public class NetworkPolicyTestUtils {
77 
78     // android.telephony.CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS
79     // TODO: Expose it as a @TestApi instead of copying the constant
80     private static final String KEY_CARRIER_METERED_APN_TYPES_STRINGS =
81             "carrier_metered_apn_types_strings";
82 
83     private static final int TIMEOUT_CHANGE_METEREDNESS_MS = 10_000;
84 
85     private static ConnectivityManager mCm;
86     private static WifiManager mWm;
87     private static CarrierConfigManager mCarrierConfigManager;
88     private static NetworkPolicyManager sNpm;
89 
90     private static Boolean mBatterySaverSupported;
91     private static Boolean mDataSaverSupported;
92     private static Boolean mDozeModeSupported;
93     private static Boolean mAppStandbySupported;
94 
NetworkPolicyTestUtils()95     private NetworkPolicyTestUtils() {}
96 
isBatterySaverSupported()97     public static boolean isBatterySaverSupported() {
98         if (mBatterySaverSupported == null) {
99             mBatterySaverSupported = BatteryUtils.isBatterySaverSupported();
100         }
101         return mBatterySaverSupported;
102     }
103 
isWear()104     private static boolean isWear() {
105         return getContext().getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
106     }
107 
108     /**
109      * As per CDD requirements, if the device doesn't support data saver mode then
110      * ConnectivityManager.getRestrictBackgroundStatus() will always return
111      * RESTRICT_BACKGROUND_STATUS_DISABLED. So, enable the data saver mode and check if
112      * ConnectivityManager.getRestrictBackgroundStatus() for an app in background returns
113      * RESTRICT_BACKGROUND_STATUS_DISABLED or not.
114      */
isDataSaverSupported()115     public static boolean isDataSaverSupported() {
116         if (isWear()) {
117             return false;
118         }
119         if (mDataSaverSupported == null) {
120             setRestrictBackgroundInternal(false);
121             assertMyRestrictBackgroundStatus(RESTRICT_BACKGROUND_STATUS_DISABLED);
122             try {
123                 setRestrictBackgroundInternal(true);
124                 mDataSaverSupported = !isMyRestrictBackgroundStatus(
125                         RESTRICT_BACKGROUND_STATUS_DISABLED);
126             } finally {
127                 setRestrictBackgroundInternal(false);
128             }
129         }
130         return mDataSaverSupported;
131     }
132 
isDozeModeSupported()133     public static boolean isDozeModeSupported() {
134         if (mDozeModeSupported == null) {
135             final String result = executeShellCommand("cmd deviceidle enabled deep");
136             mDozeModeSupported = result.equals("1");
137         }
138         return mDozeModeSupported;
139     }
140 
isAppStandbySupported()141     public static boolean isAppStandbySupported() {
142         if (mAppStandbySupported == null) {
143             mAppStandbySupported = AppStandbyUtils.isAppStandbyEnabled();
144         }
145         return mAppStandbySupported;
146     }
147 
isLowRamDevice()148     public static boolean isLowRamDevice() {
149         final ActivityManager am = (ActivityManager) getContext().getSystemService(
150                 Context.ACTIVITY_SERVICE);
151         return am.isLowRamDevice();
152     }
153 
154     /** Forces JobScheduler to run the job if constraints are met. */
forceRunJob(String pkg, int jobId)155     public static void forceRunJob(String pkg, int jobId) {
156         executeShellCommand("cmd jobscheduler run -f -u " + UserHandle.myUserId()
157                 + " " + pkg + " " + jobId);
158     }
159 
isLocationEnabled()160     public static boolean isLocationEnabled() {
161         final LocationManager lm = (LocationManager) getContext().getSystemService(
162                 Context.LOCATION_SERVICE);
163         return lm.isLocationEnabled();
164     }
165 
setLocationEnabled(boolean enabled)166     public static void setLocationEnabled(boolean enabled) {
167         final LocationManager lm = (LocationManager) getContext().getSystemService(
168                 Context.LOCATION_SERVICE);
169         lm.setLocationEnabledForUser(enabled, Process.myUserHandle());
170         assertEquals("Couldn't change location enabled state", lm.isLocationEnabled(), enabled);
171         Log.d(TAG, "Changed location enabled state to " + enabled);
172     }
173 
isActiveNetworkMetered(boolean metered)174     public static boolean isActiveNetworkMetered(boolean metered) {
175         return getConnectivityManager().isActiveNetworkMetered() == metered;
176     }
177 
canChangeActiveNetworkMeteredness()178     public static boolean canChangeActiveNetworkMeteredness() {
179         final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
180         return networkCapabilities.hasTransport(TRANSPORT_WIFI)
181                 || networkCapabilities.hasTransport(TRANSPORT_CELLULAR);
182     }
183 
184     /**
185      * Updates the meteredness of the active network. Right now we can only change meteredness
186      * of either Wifi or cellular network, so if the active network is not either of these, this
187      * will throw an exception.
188      *
189      * @return a {@link ThrowingRunnable} object that can used to reset the meteredness change
190      *         made by this method.
191      */
setupActiveNetworkMeteredness(boolean metered)192     public static ThrowingRunnable setupActiveNetworkMeteredness(boolean metered) throws Exception {
193         if (isActiveNetworkMetered(metered)) {
194             return null;
195         }
196         final NetworkCapabilities networkCapabilities = getActiveNetworkCapabilities();
197         if (networkCapabilities.hasTransport(TRANSPORT_WIFI)) {
198             final String ssid = getWifiSsid();
199             setWifiMeteredStatus(ssid, metered);
200             return () -> setWifiMeteredStatus(ssid, !metered);
201         } else if (networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
202             final int subId = SubscriptionManager.getActiveDataSubscriptionId();
203             setCellularMeteredStatus(subId, metered);
204             return () -> setCellularMeteredStatus(subId, !metered);
205         } else {
206             // Right now, we don't have a way to change meteredness of networks other
207             // than Wi-Fi or Cellular, so just throw an exception.
208             throw new IllegalStateException("Can't change meteredness of current active network");
209         }
210     }
211 
getWifiSsid()212     private static String getWifiSsid() {
213         final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
214         try {
215             uiAutomation.adoptShellPermissionIdentity();
216             final String ssid = getWifiManager().getConnectionInfo().getSSID();
217             assertNotEquals(WifiManager.UNKNOWN_SSID, ssid);
218             return ssid;
219         } finally {
220             uiAutomation.dropShellPermissionIdentity();
221         }
222     }
223 
getActiveNetworkCapabilities()224     static NetworkCapabilities getActiveNetworkCapabilities() {
225         final Network activeNetwork = getConnectivityManager().getActiveNetwork();
226         assertNotNull("No active network available", activeNetwork);
227         return getConnectivityManager().getNetworkCapabilities(activeNetwork);
228     }
229 
setWifiMeteredStatus(String ssid, boolean metered)230     private static void setWifiMeteredStatus(String ssid, boolean metered) throws Exception {
231         final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
232         try {
233             uiAutomation.adoptShellPermissionIdentity();
234             final WifiConfiguration currentConfig = getWifiConfiguration(ssid);
235             currentConfig.meteredOverride = metered
236                     ? METERED_OVERRIDE_METERED : METERED_OVERRIDE_NONE;
237             BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
238             getWifiManager().save(currentConfig, createActionListener(
239                     blockingQueue, Integer.MAX_VALUE));
240             Integer resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
241                     TimeUnit.MILLISECONDS);
242             if (resultCode == null) {
243                 fail("Timed out waiting for meteredness to change; ssid=" + ssid
244                         + ", metered=" + metered);
245             } else if (resultCode != Integer.MAX_VALUE) {
246                 fail("Error overriding the meteredness; ssid=" + ssid
247                         + ", metered=" + metered + ", error=" + resultCode);
248             }
249             final boolean success = assertActiveNetworkMetered(metered, false /* throwOnFailure */);
250             if (!success) {
251                 Log.i(TAG, "Retry connecting to wifi; ssid=" + ssid);
252                 blockingQueue = new LinkedBlockingQueue<>();
253                 getWifiManager().connect(currentConfig, createActionListener(
254                         blockingQueue, Integer.MAX_VALUE));
255                 resultCode = blockingQueue.poll(TIMEOUT_CHANGE_METEREDNESS_MS,
256                         TimeUnit.MILLISECONDS);
257                 if (resultCode == null) {
258                     fail("Timed out waiting for wifi to connect; ssid=" + ssid);
259                 } else if (resultCode != Integer.MAX_VALUE) {
260                     fail("Error connecting to wifi; ssid=" + ssid
261                             + ", error=" + resultCode);
262                 }
263                 assertActiveNetworkMetered(metered, true /* throwOnFailure */);
264             }
265         } finally {
266             uiAutomation.dropShellPermissionIdentity();
267         }
268     }
269 
getWifiConfiguration(String ssid)270     private static WifiConfiguration getWifiConfiguration(String ssid) {
271         final List<String> ssids = new ArrayList<>();
272         for (WifiConfiguration config : getWifiManager().getConfiguredNetworks()) {
273             if (config.SSID.equals(ssid)) {
274                 return config;
275             }
276             ssids.add(config.SSID);
277         }
278         fail("Couldn't find the wifi config; ssid=" + ssid
279                 + ", all=" + Arrays.toString(ssids.toArray()));
280         return null;
281     }
282 
createActionListener(BlockingQueue<Integer> blockingQueue, int successCode)283     private static ActionListener createActionListener(BlockingQueue<Integer> blockingQueue,
284             int successCode) {
285         return new ActionListener() {
286             @Override
287             public void onSuccess() {
288                 blockingQueue.offer(successCode);
289             }
290 
291             @Override
292             public void onFailure(int reason) {
293                 blockingQueue.offer(reason);
294             }
295         };
296     }
297 
298     private static void setCellularMeteredStatus(int subId, boolean metered) throws Exception {
299         final PersistableBundle bundle = new PersistableBundle();
300         bundle.putStringArray(KEY_CARRIER_METERED_APN_TYPES_STRINGS,
301                 new String[] {ApnSetting.TYPE_MMS_STRING});
302         ShellIdentityUtils.invokeMethodWithShellPermissionsNoReturn(getCarrierConfigManager(),
303                 (cm) -> cm.overrideConfig(subId, metered ? null : bundle));
304         assertActiveNetworkMetered(metered, true /* throwOnFailure */);
305     }
306 
307     private static boolean assertActiveNetworkMetered(boolean expectedMeteredStatus,
308             boolean throwOnFailure) throws Exception {
309         final CountDownLatch latch = new CountDownLatch(1);
310         final NetworkCallback networkCallback = new NetworkCallback() {
311             @Override
312             public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
313                 final boolean metered = !nc.hasCapability(NET_CAPABILITY_NOT_METERED);
314                 if (metered == expectedMeteredStatus) {
315                     latch.countDown();
316                 }
317             }
318         };
319         // Registering a callback here guarantees onCapabilitiesChanged is called immediately
320         // with the current setting. Therefore, if the setting has already been changed,
321         // this method will return right away, and if not it will wait for the setting to change.
322         getConnectivityManager().registerDefaultNetworkCallback(networkCallback);
323         try {
324             if (!latch.await(TIMEOUT_CHANGE_METEREDNESS_MS, TimeUnit.MILLISECONDS)) {
325                 final String errorMsg = "Timed out waiting for active network metered status "
326                         + "to change to " + expectedMeteredStatus + "; network = "
327                         + getConnectivityManager().getActiveNetwork();
328                 if (throwOnFailure) {
329                     fail(errorMsg);
330                 }
331                 Log.w(TAG, errorMsg);
332                 return false;
333             }
334             return true;
335         } finally {
336             getConnectivityManager().unregisterNetworkCallback(networkCallback);
337         }
338     }
339 
340     public static void setRestrictBackground(boolean enabled) {
341         if (!isDataSaverSupported()) {
342             return;
343         }
344         setRestrictBackgroundInternal(enabled);
345     }
346 
347     static void setRestrictBackgroundInternal(boolean enabled) {
348         executeShellCommand("cmd netpolicy set restrict-background " + enabled);
349         final String output = executeShellCommand("cmd netpolicy get restrict-background");
350         final String expectedSuffix = enabled ? "enabled" : "disabled";
351         assertTrue("output '" + output + "' should end with '" + expectedSuffix + "'",
352                 output.endsWith(expectedSuffix));
353     }
354 
355     public static boolean isMyRestrictBackgroundStatus(int expectedStatus) {
356         final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
357         if (expectedStatus != actualStatus) {
358             Log.d(TAG, "MyRestrictBackgroundStatus: "
359                     + "Expected: " + restrictBackgroundValueToString(expectedStatus)
360                     + "; Actual: " + restrictBackgroundValueToString(actualStatus));
361             return false;
362         }
363         return true;
364     }
365 
366     // Copied from cts/tests/tests/net/src/android/net/cts/ConnectivityManagerTest.java
367     private static String unquoteSSID(String ssid) {
368         // SSID is returned surrounded by quotes if it can be decoded as UTF-8.
369         // Otherwise it's guaranteed not to start with a quote.
370         if (ssid.charAt(0) == '"') {
371             return ssid.substring(1, ssid.length() - 1);
372         } else {
373             return ssid;
374         }
375     }
376 
377     public static String restrictBackgroundValueToString(int status) {
378         switch (status) {
379             case RESTRICT_BACKGROUND_STATUS_DISABLED:
380                 return "DISABLED";
381             case RESTRICT_BACKGROUND_STATUS_WHITELISTED:
382                 return "WHITELISTED";
383             case RESTRICT_BACKGROUND_STATUS_ENABLED:
384                 return "ENABLED";
385             default:
386                 return "UNKNOWN_STATUS_" + status;
387         }
388     }
389 
390     public static void clearSnoozeTimestamps() {
391         executeShellCommand("dumpsys netpolicy --unsnooze");
392     }
393 
394     public static String executeShellCommand(String command) {
395         final String result = runShellCommandOrThrow(command).trim();
396         Log.d(TAG, "Output of '" + command + "': '" + result + "'");
397         return result;
398     }
399 
400     public static void assertMyRestrictBackgroundStatus(int expectedStatus) {
401         final int actualStatus = getConnectivityManager().getRestrictBackgroundStatus();
402         assertEquals(restrictBackgroundValueToString(expectedStatus),
403                 restrictBackgroundValueToString(actualStatus));
404     }
405 
406     public static ConnectivityManager getConnectivityManager() {
407         if (mCm == null) {
408             mCm = (ConnectivityManager) getContext().getSystemService(Context.CONNECTIVITY_SERVICE);
409         }
410         return mCm;
411     }
412 
413     public static WifiManager getWifiManager() {
414         if (mWm == null) {
415             mWm = (WifiManager) getContext().getSystemService(Context.WIFI_SERVICE);
416         }
417         return mWm;
418     }
419 
420     public static CarrierConfigManager getCarrierConfigManager() {
421         if (mCarrierConfigManager == null) {
422             mCarrierConfigManager = (CarrierConfigManager) getContext().getSystemService(
423                     Context.CARRIER_CONFIG_SERVICE);
424         }
425         return mCarrierConfigManager;
426     }
427 
428     public static NetworkPolicyManager getNetworkPolicyManager() {
429         if (sNpm == null) {
430             sNpm = getContext().getSystemService(NetworkPolicyManager.class);
431         }
432         return sNpm;
433     }
434 
435     public static Context getContext() {
436         return getInstrumentation().getContext();
437     }
438 
439     public static Instrumentation getInstrumentation() {
440         return InstrumentationRegistry.getInstrumentation();
441     }
442 
443     public static UiDevice getUiDevice() {
444         return UiDevice.getInstance(getInstrumentation());
445     }
446 
447     // When power saver mode or restrict background enabled or adding any white/black list into
448     // those modes, NetworkPolicy may need to take some time to update the rules of uids. So having
449     // this function and using PollingCheck to try to make sure the uid has updated and reduce the
450     // flaky rate.
451     public static void assertNetworkingBlockedStatusForUid(int uid, boolean metered,
452             boolean expectedResult) {
453         final String errMsg = String.format("Unexpected result from isUidNetworkingBlocked; "
454                 + "uid= " + uid + ", metered=" + metered + ", expected=" + expectedResult);
455         PollingCheck.waitFor(() -> (expectedResult == isUidNetworkingBlocked(uid, metered)),
456                 errMsg);
457     }
458 
459     public static void assertIsUidRestrictedOnMeteredNetworks(int uid, boolean expectedResult) {
460         final String errMsg = String.format(
461                 "Unexpected result from isUidRestrictedOnMeteredNetworks; "
462                 + "uid= " + uid + ", expected=" + expectedResult);
463         PollingCheck.waitFor(() -> (expectedResult == isUidRestrictedOnMeteredNetworks(uid)),
464                 errMsg);
465     }
466 
467     public static boolean isUidNetworkingBlocked(int uid, boolean meteredNetwork) {
468         final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
469         try {
470             uiAutomation.adoptShellPermissionIdentity();
471             return getNetworkPolicyManager().isUidNetworkingBlocked(uid, meteredNetwork);
472         } finally {
473             uiAutomation.dropShellPermissionIdentity();
474         }
475     }
476 
477     public static boolean isUidRestrictedOnMeteredNetworks(int uid) {
478         final UiAutomation uiAutomation = getInstrumentation().getUiAutomation();
479         try {
480             uiAutomation.adoptShellPermissionIdentity();
481             return getNetworkPolicyManager().isUidRestrictedOnMeteredNetworks(uid);
482         } finally {
483             uiAutomation.dropShellPermissionIdentity();
484         }
485     }
486 }
487