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