1 /*
2  * Copyright (C) 2021 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.net.cts;
18 
19 import static android.Manifest.permission.UPDATE_DEVICE_STATS;
20 import static android.content.pm.PackageManager.FEATURE_TELEPHONY;
21 import static android.content.pm.PackageManager.FEATURE_WIFI;
22 
23 import static androidx.test.InstrumentationRegistry.getContext;
24 
25 import static com.android.compatibility.common.util.BatteryUtils.hasBattery;
26 import static com.android.compatibility.common.util.SystemUtil.runShellCommand;
27 import static com.android.testutils.MiscAsserts.assertThrows;
28 import static com.android.testutils.TestPermissionUtil.runAsShell;
29 
30 import static org.junit.Assert.assertEquals;
31 import static org.junit.Assert.fail;
32 import static org.junit.Assume.assumeTrue;
33 
34 import android.content.Context;
35 import android.content.pm.PackageManager;
36 import android.net.ConnectivityManager;
37 import android.net.Network;
38 import android.net.cts.util.CtsNetUtils;
39 import android.net.wifi.WifiManager;
40 import android.os.BatteryStatsManager;
41 import android.os.Build;
42 import android.os.connectivity.CellularBatteryStats;
43 import android.os.connectivity.WifiBatteryStats;
44 import android.platform.test.annotations.AppModeFull;
45 import android.util.Log;
46 
47 import androidx.test.filters.RequiresDevice;
48 import androidx.test.filters.SdkSuppress;
49 import androidx.test.runner.AndroidJUnit4;
50 
51 import com.android.testutils.AutoReleaseNetworkCallbackRule;
52 import com.android.testutils.DevSdkIgnoreRule;
53 
54 import org.junit.Before;
55 import org.junit.Rule;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 
59 import java.io.IOException;
60 import java.net.HttpURLConnection;
61 import java.net.URL;
62 import java.util.function.Predicate;
63 import java.util.function.Supplier;
64 
65 /**
66  * Test for BatteryStatsManager.
67  */
68 @RunWith(AndroidJUnit4.class)
69 @SdkSuppress(minSdkVersion = Build.VERSION_CODES.R) // BatteryStatsManager did not exist on Q
70 public class BatteryStatsManagerTest{
71     @Rule(order = 1)
72     public final AutoReleaseNetworkCallbackRule
73             networkCallbackRule = new AutoReleaseNetworkCallbackRule();
74     @Rule(order = 2)
75     public final DevSdkIgnoreRule ignoreRule = new DevSdkIgnoreRule();
76     private static final String TAG = BatteryStatsManagerTest.class.getSimpleName();
77     private static final String TEST_URL = "https://connectivitycheck.gstatic.com/generate_204";
78     // This value should be the same as BatteryStatsManager.BATTERY_STATUS_DISCHARGING.
79     // TODO: Use the constant once it's available in all branches
80     private static final int BATTERY_STATUS_DISCHARGING = 3;
81 
82     private Context mContext;
83     private BatteryStatsManager mBsm;
84     private ConnectivityManager mCm;
85     private WifiManager mWm;
86     private PackageManager mPm;
87     private CtsNetUtils mCtsNetUtils;
88 
89     @Before
setUp()90     public void setUp() throws Exception {
91         mContext = getContext();
92         mBsm = mContext.getSystemService(BatteryStatsManager.class);
93         mCm = mContext.getSystemService(ConnectivityManager.class);
94         mWm = mContext.getSystemService(WifiManager.class);
95         mPm = mContext.getPackageManager();
96         mCtsNetUtils = new CtsNetUtils(mContext);
97     }
98 
99     // reportNetworkInterfaceForTransports classifies one network interface as wifi or mobile, so
100     // check that the interface is classified properly by checking the data usage is reported
101     // properly.
102     @Test
103     @AppModeFull(reason = "Cannot get CHANGE_NETWORK_STATE to request wifi/cell in instant mode")
104     @RequiresDevice // Virtual hardware does not support wifi battery stats
testReportNetworkInterfaceForTransports()105     public void testReportNetworkInterfaceForTransports() throws Exception {
106         try {
107             assumeTrue("Battery is not present. Ignore test.", hasBattery());
108             // Simulate the device being unplugged from charging.
109             executeShellCommand("cmd battery unplug");
110             executeShellCommand("cmd battery set status " + BATTERY_STATUS_DISCHARGING);
111             // Reset all current stats before starting test.
112             executeShellCommand("dumpsys batterystats --reset");
113             // Do not automatically reset the stats when the devices are unplugging after the
114             // battery was last full or the level is 100, or have gone through a significant
115             // charge.
116             executeShellCommand("dumpsys batterystats enable no-auto-reset");
117             // Upon calling "cmd battery unplug" a task is scheduled on the battery
118             // stats worker thread. Because network battery stats are only recorded
119             // when the device is on battery, this test needs to wait until the
120             // battery status is recorded because causing traffic.
121             // Writing stats to disk is unnecessary, but --write waits for the worker
122             // thread to finish processing the enqueued tasks as a side effect. This
123             // side effect is the point of using --write here.
124             executeShellCommand("dumpsys batterystats --write");
125 
126             if (mPm.hasSystemFeature(FEATURE_WIFI)) {
127                 // Make sure wifi is disabled.
128                 mCtsNetUtils.ensureWifiDisconnected(null /* wifiNetworkToCheck */);
129             }
130 
131             verifyGetCellBatteryStats();
132             verifyGetWifiBatteryStats();
133 
134         } finally {
135             // Reset battery settings.
136             executeShellCommand("dumpsys batterystats disable no-auto-reset");
137             executeShellCommand("cmd battery reset");
138             if (mPm.hasSystemFeature(FEATURE_WIFI)) {
139                 mCtsNetUtils.ensureWifiConnected();
140             }
141         }
142     }
143 
verifyGetCellBatteryStats()144     private void verifyGetCellBatteryStats() throws Exception {
145         final boolean isTelephonySupported = mPm.hasSystemFeature(FEATURE_TELEPHONY);
146 
147         if (!isTelephonySupported) {
148             Log.d(TAG, "Skip cell battery stats test because device does not support telephony.");
149             return;
150         }
151 
152         final Network cellNetwork = networkCallbackRule.requestCell();
153         final URL url = new URL(TEST_URL);
154 
155         // Get cellular battery stats
156         CellularBatteryStats cellularStatsBefore = runAsShell(UPDATE_DEVICE_STATS,
157                 mBsm::getCellularBatteryStats);
158 
159         // Generate traffic on cellular network.
160         Log.d(TAG, "Generate traffic on cellular network.");
161         generateNetworkTraffic(cellNetwork, url);
162 
163         // The mobile battery stats are updated when a network stops being the default network.
164         // ConnectivityService will call BatteryStatsManager.reportMobileRadioPowerState when
165         // removing data activity tracking.
166         try {
167             mCtsNetUtils.setMobileDataEnabled(false);
168 
169             // There's rate limit to update mobile battery so if ConnectivityService calls
170             // BatteryStatsManager.reportMobileRadioPowerState when default network changed,
171             // the mobile stats might not be updated. But if the mobile update due to other
172             // reasons (plug/unplug, battery level change, etc) will be unaffected. Thus here
173             // dumps the battery stats to trigger a full sync of data.
174             executeShellCommand("dumpsys batterystats");
175 
176             // Check cellular battery stats are updated.
177             runAsShell(UPDATE_DEVICE_STATS,
178                     () -> assertStatsEventually(mBsm::getCellularBatteryStats,
179                         cellularStatsAfter -> cellularBatteryStatsIncreased(
180                         cellularStatsBefore, cellularStatsAfter)));
181         } finally {
182             mCtsNetUtils.setMobileDataEnabled(true);
183         }
184     }
185 
verifyGetWifiBatteryStats()186     private void verifyGetWifiBatteryStats() throws Exception {
187         if (!mPm.hasSystemFeature(FEATURE_WIFI)) {
188             return;
189         }
190 
191         final Network wifiNetwork = mCtsNetUtils.ensureWifiConnected();
192         final URL url = new URL(TEST_URL);
193 
194         if (!mWm.isEnhancedPowerReportingSupported()) {
195             Log.d(TAG, "Skip wifi stats test because wifi does not support link layer stats.");
196             return;
197         }
198 
199         WifiBatteryStats wifiStatsBefore = runAsShell(UPDATE_DEVICE_STATS,
200                 mBsm::getWifiBatteryStats);
201 
202         // Generate traffic on wifi network.
203         Log.d(TAG, "Generate traffic on wifi network.");
204         generateNetworkTraffic(wifiNetwork, url);
205         // Wifi battery stats are updated when wifi on.
206         mCtsNetUtils.disableWifi();
207         mCtsNetUtils.ensureWifiConnected();
208 
209         // Check wifi battery stats are updated.
210         runAsShell(UPDATE_DEVICE_STATS,
211                 () -> assertStatsEventually(mBsm::getWifiBatteryStats,
212                     wifiStatsAfter -> wifiBatteryStatsIncreased(wifiStatsBefore,
213                     wifiStatsAfter)));
214     }
215 
216     @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
217     @AppModeFull(reason = "Cannot get WifiManager in instant app mode")
218     @Test
testReportNetworkInterfaceForTransports_throwsSecurityException()219     public void testReportNetworkInterfaceForTransports_throwsSecurityException()
220             throws Exception {
221         final Network network = mCm.getActiveNetwork();
222         final String iface = mCm.getLinkProperties(network).getInterfaceName();
223         final int[] transportType = mCm.getNetworkCapabilities(network).getTransportTypes();
224         assertThrows(SecurityException.class,
225                 () -> mBsm.reportNetworkInterfaceForTransports(iface, transportType));
226     }
227 
generateNetworkTraffic(Network network, URL url)228     private void generateNetworkTraffic(Network network, URL url) throws IOException {
229         HttpURLConnection connection = null;
230         try {
231             connection = (HttpURLConnection) network.openConnection(url);
232             assertEquals(204, connection.getResponseCode());
233         } catch (IOException e) {
234             Log.e(TAG, "Generate traffic failed with exception " + e);
235         } finally {
236             if (connection != null) {
237                 connection.disconnect();
238             }
239         }
240     }
241 
assertStatsEventually(Supplier<T> statsGetter, Predicate<T> statsChecker)242     private static <T> void assertStatsEventually(Supplier<T> statsGetter,
243             Predicate<T> statsChecker) throws Exception {
244         // Wait for updating mobile/wifi stats, and check stats every 10ms.
245         final int maxTries = 1000;
246         T result = null;
247         for (int i = 1; i <= maxTries; i++) {
248             result = statsGetter.get();
249             if (statsChecker.test(result)) return;
250             Thread.sleep(10);
251         }
252         final String stats = result instanceof CellularBatteryStats
253                 ? "Cellular" : "Wifi";
254         fail(stats + " battery stats did not increase.");
255     }
256 
cellularBatteryStatsIncreased(CellularBatteryStats before, CellularBatteryStats after)257     private static boolean cellularBatteryStatsIncreased(CellularBatteryStats before,
258             CellularBatteryStats after) {
259         return (after.getNumBytesTx() > before.getNumBytesTx())
260                 && (after.getNumBytesRx() > before.getNumBytesRx())
261                 && (after.getNumPacketsTx() > before.getNumPacketsTx())
262                 && (after.getNumPacketsRx() > before.getNumPacketsRx());
263     }
264 
wifiBatteryStatsIncreased(WifiBatteryStats before, WifiBatteryStats after)265     private static boolean wifiBatteryStatsIncreased(WifiBatteryStats before,
266             WifiBatteryStats after) {
267         return (after.getNumBytesTx() > before.getNumBytesTx())
268                 && (after.getNumBytesRx() > before.getNumBytesRx())
269                 && (after.getNumPacketsTx() > before.getNumPacketsTx())
270                 && (after.getNumPacketsRx() > before.getNumPacketsRx());
271     }
272 
executeShellCommand(String command)273     private static String executeShellCommand(String command) {
274         final String result = runShellCommand(command).trim();
275         Log.d(TAG, "Output of '" + command + "': '" + result + "'");
276         return result;
277     }
278 }
279