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