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