1 /* 2 * Copyright (C) 2017 Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.google.android.mobly.snippet.bundled; 18 19 import android.content.BroadcastReceiver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.IntentFilter; 23 import android.net.wifi.ScanResult; 24 import android.net.wifi.WifiConfiguration; 25 import android.net.wifi.WifiInfo; 26 import android.net.wifi.WifiManager; 27 import android.os.Build; 28 import androidx.annotation.Nullable; 29 import androidx.annotation.RequiresApi; 30 import androidx.test.platform.app.InstrumentationRegistry; 31 import com.google.android.mobly.snippet.Snippet; 32 import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; 33 import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; 34 import com.google.android.mobly.snippet.bundled.utils.Utils; 35 import com.google.android.mobly.snippet.rpc.Rpc; 36 import com.google.android.mobly.snippet.rpc.RpcMinSdk; 37 import com.google.android.mobly.snippet.util.Log; 38 import java.util.ArrayList; 39 import java.util.List; 40 import org.json.JSONArray; 41 import org.json.JSONException; 42 import org.json.JSONObject; 43 import android.net.wifi.SupplicantState; 44 45 import com.google.android.mobly.snippet.bundled.utils.Utils; 46 47 /** Snippet class exposing Android APIs in WifiManager. */ 48 public class WifiManagerSnippet implements Snippet { 49 private static class WifiManagerSnippetException extends Exception { 50 private static final long serialVersionUID = 1; 51 WifiManagerSnippetException(String msg)52 public WifiManagerSnippetException(String msg) { 53 super(msg); 54 } 55 } 56 57 private static final int TIMEOUT_TOGGLE_STATE = 30; 58 private final WifiManager mWifiManager; 59 private final Context mContext; 60 private final JsonSerializer mJsonSerializer = new JsonSerializer(); 61 private volatile boolean mIsScanResultAvailable = false; 62 WifiManagerSnippet()63 public WifiManagerSnippet() throws Throwable { 64 mContext = InstrumentationRegistry.getInstrumentation().getContext(); 65 mWifiManager = 66 (WifiManager) 67 mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); 68 Utils.adaptShellPermissionIfRequired(mContext); 69 } 70 71 @Rpc( 72 description = 73 "Clears all configured networks. This will only work if all configured " 74 + "networks were added through this MBS instance") wifiClearConfiguredNetworks()75 public void wifiClearConfiguredNetworks() throws WifiManagerSnippetException { 76 List<WifiConfiguration> unremovedConfigs = mWifiManager.getConfiguredNetworks(); 77 List<WifiConfiguration> failedConfigs = new ArrayList<>(); 78 if (unremovedConfigs == null) { 79 throw new WifiManagerSnippetException( 80 "Failed to get a list of configured networks. Is wifi disabled?"); 81 } 82 for (WifiConfiguration config : unremovedConfigs) { 83 if (!mWifiManager.removeNetwork(config.networkId)) { 84 failedConfigs.add(config); 85 } 86 } 87 88 // If removeNetwork is called on a network with both an open and OWE config, it will remove 89 // both. The subsequent call on the same network will fail. The clear operation may succeed 90 // even if failures appear in the log below. 91 if (!failedConfigs.isEmpty()) { 92 Log.e("Encountered error while removing networks: " + failedConfigs); 93 } 94 95 // Re-check configured configs list to ensure that it is cleared 96 unremovedConfigs = mWifiManager.getConfiguredNetworks(); 97 if (!unremovedConfigs.isEmpty()) { 98 throw new WifiManagerSnippetException("Failed to remove networks: " + unremovedConfigs); 99 } 100 } 101 102 @Rpc(description = "Turns on Wi-Fi with a 30s timeout.") wifiEnable()103 public void wifiEnable() throws InterruptedException, WifiManagerSnippetException { 104 if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { 105 return; 106 } 107 // If Wi-Fi is trying to turn off, wait for that to complete before continuing. 108 if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLING) { 109 if (!Utils.waitUntil( 110 () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, 111 TIMEOUT_TOGGLE_STATE)) { 112 Log.e(String.format("Wi-Fi failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); 113 } 114 } 115 if (!mWifiManager.setWifiEnabled(true)) { 116 throw new WifiManagerSnippetException("Failed to initiate enabling Wi-Fi."); 117 } 118 if (!Utils.waitUntil( 119 () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, 120 TIMEOUT_TOGGLE_STATE)) { 121 throw new WifiManagerSnippetException( 122 String.format( 123 "Failed to enable Wi-Fi after %ss, timeout!", TIMEOUT_TOGGLE_STATE)); 124 } 125 } 126 127 @Rpc(description = "Turns off Wi-Fi with a 30s timeout.") wifiDisable()128 public void wifiDisable() throws InterruptedException, WifiManagerSnippetException { 129 if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) { 130 return; 131 } 132 // If Wi-Fi is trying to turn on, wait for that to complete before continuing. 133 if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLING) { 134 if (!Utils.waitUntil( 135 () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, 136 TIMEOUT_TOGGLE_STATE)) { 137 Log.e(String.format("Wi-Fi failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); 138 } 139 } 140 if (!mWifiManager.setWifiEnabled(false)) { 141 throw new WifiManagerSnippetException("Failed to initiate disabling Wi-Fi."); 142 } 143 if (!Utils.waitUntil( 144 () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, 145 TIMEOUT_TOGGLE_STATE)) { 146 throw new WifiManagerSnippetException( 147 String.format( 148 "Failed to disable Wi-Fi after %ss, timeout!", TIMEOUT_TOGGLE_STATE)); 149 } 150 } 151 152 @Rpc(description = "Checks if Wi-Fi is enabled.") wifiIsEnabled()153 public boolean wifiIsEnabled() { 154 return mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED; 155 } 156 157 @Rpc(description = "Trigger Wi-Fi scan.") wifiStartScan()158 public void wifiStartScan() throws WifiManagerSnippetException { 159 if (!mWifiManager.startScan()) { 160 throw new WifiManagerSnippetException("Failed to initiate Wi-Fi scan."); 161 } 162 } 163 164 @Rpc( 165 description = 166 "Get Wi-Fi scan results, which is a list of serialized WifiScanResult objects.") wifiGetCachedScanResults()167 public JSONArray wifiGetCachedScanResults() throws JSONException { 168 JSONArray results = new JSONArray(); 169 for (ScanResult result : mWifiManager.getScanResults()) { 170 results.put(mJsonSerializer.toJson(result)); 171 } 172 return results; 173 } 174 175 @Rpc( 176 description = 177 "Start scan, wait for scan to complete, and return results, which is a list of " 178 + "serialized WifiScanResult objects.") wifiScanAndGetResults()179 public JSONArray wifiScanAndGetResults() 180 throws InterruptedException, JSONException, WifiManagerSnippetException { 181 mContext.registerReceiver( 182 new WifiScanReceiver(), 183 new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); 184 wifiStartScan(); 185 mIsScanResultAvailable = false; 186 if (!Utils.waitUntil(() -> mIsScanResultAvailable, 2 * 60)) { 187 throw new WifiManagerSnippetException( 188 "Failed to get scan results after 2min, timeout!"); 189 } 190 return wifiGetCachedScanResults(); 191 } 192 193 @Rpc( 194 description = 195 "Connects to a Wi-Fi network. This covers the common network types like open and " 196 + "WPA2.") wifiConnectSimple(String ssid, @Nullable String password)197 public void wifiConnectSimple(String ssid, @Nullable String password) 198 throws InterruptedException, JSONException, WifiManagerSnippetException { 199 JSONObject config = new JSONObject(); 200 config.put("SSID", ssid); 201 if (password != null) { 202 config.put("password", password); 203 } 204 wifiConnect(config); 205 } 206 207 /** 208 * Gets the {@link WifiConfiguration} of a Wi-Fi network that has already been configured. 209 * 210 * <p>If the network has not been configured, returns null. 211 * 212 * <p>A network is configured if a WifiConfiguration was created for it and added with {@link 213 * WifiManager#addNetwork(WifiConfiguration)}. 214 */ getExistingConfiguredNetwork(String ssid)215 private WifiConfiguration getExistingConfiguredNetwork(String ssid) { 216 List<WifiConfiguration> wifiConfigs = mWifiManager.getConfiguredNetworks(); 217 if (wifiConfigs == null) { 218 return null; 219 } 220 for (WifiConfiguration config : wifiConfigs) { 221 if (config.SSID.equals(ssid)) { 222 return config; 223 } 224 } 225 return null; 226 } 227 /** 228 * Connect to a Wi-Fi network. 229 * 230 * @param wifiNetworkConfig A JSON object that contains the info required to connect to a Wi-Fi 231 * network. It follows the fields of WifiConfiguration type, e.g. {"SSID": "myWifi", 232 * "password": "12345678"}. 233 * @throws InterruptedException 234 * @throws JSONException 235 * @throws WifiManagerSnippetException 236 */ 237 @Rpc(description = "Connects to a Wi-Fi network.") wifiConnect(JSONObject wifiNetworkConfig)238 public void wifiConnect(JSONObject wifiNetworkConfig) 239 throws InterruptedException, JSONException, WifiManagerSnippetException { 240 Log.d("Got network config: " + wifiNetworkConfig); 241 WifiConfiguration wifiConfig = JsonDeserializer.jsonToWifiConfig(wifiNetworkConfig); 242 String SSID = wifiConfig.SSID; 243 // Return directly if network is already connected. 244 WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); 245 if (connectionInfo.getNetworkId() != -1 246 && connectionInfo.getSSID().equals(wifiConfig.SSID)) { 247 Log.d("Network " + connectionInfo.getSSID() + " is already connected."); 248 return; 249 } 250 int networkId; 251 // If this is a network with a known SSID, connect with the existing config. 252 // We have to do this because in N+, network configs can only be modified by the UID that 253 // created the network. So any attempt to modify a network config that does not belong to us 254 // would result in error. 255 WifiConfiguration existingConfig = getExistingConfiguredNetwork(wifiConfig.SSID); 256 if (existingConfig != null) { 257 Log.w( 258 "Connecting to network \"" 259 + existingConfig.SSID 260 + "\" with its existing configuration: " 261 + existingConfig.toString()); 262 wifiConfig = existingConfig; 263 networkId = wifiConfig.networkId; 264 } else { 265 // If this is a network with a new SSID, add the network. 266 networkId = mWifiManager.addNetwork(wifiConfig); 267 } 268 mWifiManager.disconnect(); 269 if (!mWifiManager.enableNetwork(networkId, true)) { 270 throw new WifiManagerSnippetException( 271 "Failed to enable Wi-Fi network of ID: " + networkId); 272 } 273 if (!mWifiManager.reconnect()) { 274 throw new WifiManagerSnippetException( 275 "Failed to reconnect to Wi-Fi network of ID: " + networkId); 276 } 277 if (!Utils.waitUntil( 278 () -> 279 mWifiManager.getConnectionInfo().getSSID().equals(SSID) 280 && mWifiManager.getConnectionInfo().getNetworkId() != -1 && mWifiManager 281 .getConnectionInfo().getSupplicantState().equals(SupplicantState.COMPLETED), 282 90)) { 283 throw new WifiManagerSnippetException( 284 String.format( 285 "Failed to connect to '%s', timeout! Current connection: '%s'", 286 wifiNetworkConfig, mWifiManager.getConnectionInfo().getSSID())); 287 } 288 Log.d( 289 "Connected to network '" 290 + mWifiManager.getConnectionInfo().getSSID() 291 + "' with ID " 292 + mWifiManager.getConnectionInfo().getNetworkId()); 293 } 294 295 @Rpc( 296 description = 297 "Forget a configured Wi-Fi network by its network ID, which is part of the" 298 + " WifiConfiguration.") wifiRemoveNetwork(Integer networkId)299 public void wifiRemoveNetwork(Integer networkId) throws WifiManagerSnippetException { 300 if (!mWifiManager.removeNetwork(networkId)) { 301 throw new WifiManagerSnippetException("Failed to remove network of ID: " + networkId); 302 } 303 } 304 305 @Rpc( 306 description = 307 "Get the list of configured Wi-Fi networks, each is a serialized " 308 + "WifiConfiguration object.") wifiGetConfiguredNetworks()309 public List<JSONObject> wifiGetConfiguredNetworks() throws JSONException { 310 List<JSONObject> networks = new ArrayList<>(); 311 for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { 312 networks.add(mJsonSerializer.toJson(config)); 313 } 314 return networks; 315 } 316 317 @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP) 318 @Rpc(description = "Enable or disable wifi verbose logging.") wifiSetVerboseLogging(boolean enable)319 public void wifiSetVerboseLogging(boolean enable) throws Throwable { 320 Utils.invokeByReflection(mWifiManager, "enableVerboseLogging", enable ? 1 : 0); 321 } 322 323 @Rpc( 324 description = 325 "Get the information about the active Wi-Fi connection, which is a serialized " 326 + "WifiInfo object.") wifiGetConnectionInfo()327 public JSONObject wifiGetConnectionInfo() throws JSONException { 328 return mJsonSerializer.toJson(mWifiManager.getConnectionInfo()); 329 } 330 331 @Rpc( 332 description = 333 "Get the info from last successful DHCP request, which is a serialized DhcpInfo " 334 + "object.") wifiGetDhcpInfo()335 public JSONObject wifiGetDhcpInfo() throws JSONException { 336 return mJsonSerializer.toJson(mWifiManager.getDhcpInfo()); 337 } 338 339 @Rpc(description = "Check whether Wi-Fi Soft AP (hotspot) is enabled.") wifiIsApEnabled()340 public boolean wifiIsApEnabled() throws Throwable { 341 return (boolean) Utils.invokeByReflection(mWifiManager, "isWifiApEnabled"); 342 } 343 344 @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 345 @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP) 346 @Rpc( 347 description = 348 "Check whether this device supports 5 GHz band Wi-Fi. " 349 + "Turn on Wi-Fi before calling.") wifiIs5GHzBandSupported()350 public boolean wifiIs5GHzBandSupported() { 351 return mWifiManager.is5GHzBandSupported(); 352 } 353 354 /** 355 * Enable Wi-Fi Soft AP (hotspot). 356 * 357 * @param configuration The same format as the param wifiNetworkConfig param for wifiConnect. 358 * @throws Throwable 359 */ 360 @Rpc(description = "Enable Wi-Fi Soft AP (hotspot).") wifiEnableSoftAp(@ullable JSONObject configuration)361 public void wifiEnableSoftAp(@Nullable JSONObject configuration) throws Throwable { 362 // If no configuration is provided, the existing configuration would be used. 363 WifiConfiguration wifiConfiguration = null; 364 if (configuration != null) { 365 wifiConfiguration = JsonDeserializer.jsonToWifiConfig(configuration); 366 // Have to trim off the extra quotation marks since Soft AP logic interprets 367 // WifiConfiguration.SSID literally, unlike the WifiManager connection logic. 368 wifiConfiguration.SSID = JsonSerializer.trimQuotationMarks(wifiConfiguration.SSID); 369 } 370 if (!(boolean) 371 Utils.invokeByReflection( 372 mWifiManager, "setWifiApEnabled", wifiConfiguration, true)) { 373 throw new WifiManagerSnippetException("Failed to initiate turning on Wi-Fi Soft AP."); 374 } 375 if (!Utils.waitUntil(() -> wifiIsApEnabled() == true, 60)) { 376 throw new WifiManagerSnippetException( 377 "Timed out after 60s waiting for Wi-Fi Soft AP state to turn on with configuration: " 378 + configuration); 379 } 380 } 381 382 /** Disables Wi-Fi Soft AP (hotspot). */ 383 @Rpc(description = "Disable Wi-Fi Soft AP (hotspot).") wifiDisableSoftAp()384 public void wifiDisableSoftAp() throws Throwable { 385 if (!(boolean) 386 Utils.invokeByReflection( 387 mWifiManager, 388 "setWifiApEnabled", 389 null /* No configuration needed for disabling */, 390 false)) { 391 throw new WifiManagerSnippetException("Failed to initiate turning off Wi-Fi Soft AP."); 392 } 393 if (!Utils.waitUntil(() -> wifiIsApEnabled() == false, 60)) { 394 throw new WifiManagerSnippetException( 395 "Timed out after 60s waiting for Wi-Fi Soft AP state to turn off."); 396 } 397 } 398 399 @Override shutdown()400 public void shutdown() {} 401 402 403 private class WifiScanReceiver extends BroadcastReceiver { 404 405 @Override onReceive(Context c, Intent intent)406 public void onReceive(Context c, Intent intent) { 407 String action = intent.getAction(); 408 if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { 409 mIsScanResultAvailable = true; 410 } 411 } 412 } 413 } 414