diff options
Diffstat (limited to 'src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java')
-rw-r--r-- | src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java | 434 |
1 files changed, 434 insertions, 0 deletions
diff --git a/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java new file mode 100644 index 0000000..cf577c3 --- /dev/null +++ b/src/main/java/com/google/android/mobly/snippet/bundled/WifiManagerSnippet.java @@ -0,0 +1,434 @@ +/* + * Copyright (C) 2017 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ + +package com.google.android.mobly.snippet.bundled; + +import android.app.UiAutomation; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.wifi.ScanResult; +import android.net.wifi.WifiConfiguration; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.test.platform.app.InstrumentationRegistry; +import com.google.android.mobly.snippet.Snippet; +import com.google.android.mobly.snippet.bundled.utils.JsonDeserializer; +import com.google.android.mobly.snippet.bundled.utils.JsonSerializer; +import com.google.android.mobly.snippet.bundled.utils.Utils; +import com.google.android.mobly.snippet.rpc.Rpc; +import com.google.android.mobly.snippet.rpc.RpcMinSdk; +import com.google.android.mobly.snippet.util.Log; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import android.net.wifi.SupplicantState; +/** Snippet class exposing Android APIs in WifiManager. */ +public class WifiManagerSnippet implements Snippet { + private static class WifiManagerSnippetException extends Exception { + private static final long serialVersionUID = 1; + + public WifiManagerSnippetException(String msg) { + super(msg); + } + + public WifiManagerSnippetException(String msg, Throwable err) { + super(msg, err); + } + } + + private static final int TIMEOUT_TOGGLE_STATE = 30; + private final WifiManager mWifiManager; + private final Context mContext; + private final JsonSerializer mJsonSerializer = new JsonSerializer(); + private volatile boolean mIsScanResultAvailable = false; + + public WifiManagerSnippet() throws Throwable { + mContext = InstrumentationRegistry.getInstrumentation().getContext(); + mWifiManager = + (WifiManager) + mContext.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + adaptShellPermissionIfRequired(); + } + + @Rpc( + description = + "Clears all configured networks. This will only work if all configured " + + "networks were added through this MBS instance") + public void wifiClearConfiguredNetworks() throws WifiManagerSnippetException { + List<WifiConfiguration> unremovedConfigs = mWifiManager.getConfiguredNetworks(); + List<WifiConfiguration> failedConfigs = new ArrayList<>(); + if (unremovedConfigs == null) { + throw new WifiManagerSnippetException( + "Failed to get a list of configured networks. Is wifi disabled?"); + } + for (WifiConfiguration config : unremovedConfigs) { + if (!mWifiManager.removeNetwork(config.networkId)) { + failedConfigs.add(config); + } + } + if (!failedConfigs.isEmpty()) { + throw new WifiManagerSnippetException("Failed to remove networks: " + failedConfigs); + } + } + + @Rpc(description = "Turns on Wi-Fi with a 30s timeout.") + public void wifiEnable() throws InterruptedException, WifiManagerSnippetException { + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED) { + return; + } + // If Wi-Fi is trying to turn off, wait for that to complete before continuing. + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLING) { + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, + TIMEOUT_TOGGLE_STATE)) { + Log.e(String.format("Wi-Fi failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); + } + } + if (!mWifiManager.setWifiEnabled(true)) { + throw new WifiManagerSnippetException("Failed to initiate enabling Wi-Fi."); + } + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, + TIMEOUT_TOGGLE_STATE)) { + throw new WifiManagerSnippetException( + String.format( + "Failed to enable Wi-Fi after %ss, timeout!", TIMEOUT_TOGGLE_STATE)); + } + } + + @Rpc(description = "Turns off Wi-Fi with a 30s timeout.") + public void wifiDisable() throws InterruptedException, WifiManagerSnippetException { + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED) { + return; + } + // If Wi-Fi is trying to turn on, wait for that to complete before continuing. + if (mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLING) { + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED, + TIMEOUT_TOGGLE_STATE)) { + Log.e(String.format("Wi-Fi failed to stabilize after %ss.", TIMEOUT_TOGGLE_STATE)); + } + } + if (!mWifiManager.setWifiEnabled(false)) { + throw new WifiManagerSnippetException("Failed to initiate disabling Wi-Fi."); + } + if (!Utils.waitUntil( + () -> mWifiManager.getWifiState() == WifiManager.WIFI_STATE_DISABLED, + TIMEOUT_TOGGLE_STATE)) { + throw new WifiManagerSnippetException( + String.format( + "Failed to disable Wi-Fi after %ss, timeout!", TIMEOUT_TOGGLE_STATE)); + } + } + + @Rpc(description = "Checks if Wi-Fi is enabled.") + public boolean wifiIsEnabled() { + return mWifiManager.getWifiState() == WifiManager.WIFI_STATE_ENABLED; + } + + @Rpc(description = "Trigger Wi-Fi scan.") + public void wifiStartScan() throws WifiManagerSnippetException { + if (!mWifiManager.startScan()) { + throw new WifiManagerSnippetException("Failed to initiate Wi-Fi scan."); + } + } + + @Rpc( + description = + "Get Wi-Fi scan results, which is a list of serialized WifiScanResult objects.") + public JSONArray wifiGetCachedScanResults() throws JSONException { + JSONArray results = new JSONArray(); + for (ScanResult result : mWifiManager.getScanResults()) { + results.put(mJsonSerializer.toJson(result)); + } + return results; + } + + @Rpc( + description = + "Start scan, wait for scan to complete, and return results, which is a list of " + + "serialized WifiScanResult objects.") + public JSONArray wifiScanAndGetResults() + throws InterruptedException, JSONException, WifiManagerSnippetException { + mContext.registerReceiver( + new WifiScanReceiver(), + new IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)); + wifiStartScan(); + mIsScanResultAvailable = false; + if (!Utils.waitUntil(() -> mIsScanResultAvailable, 2 * 60)) { + throw new WifiManagerSnippetException( + "Failed to get scan results after 2min, timeout!"); + } + return wifiGetCachedScanResults(); + } + + @Rpc( + description = + "Connects to a Wi-Fi network. This covers the common network types like open and " + + "WPA2.") + public void wifiConnectSimple(String ssid, @Nullable String password) + throws InterruptedException, JSONException, WifiManagerSnippetException { + JSONObject config = new JSONObject(); + config.put("SSID", ssid); + if (password != null) { + config.put("password", password); + } + wifiConnect(config); + } + + /** + * Gets the {@link WifiConfiguration} of a Wi-Fi network that has already been configured. + * + * <p>If the network has not been configured, returns null. + * + * <p>A network is configured if a WifiConfiguration was created for it and added with {@link + * WifiManager#addNetwork(WifiConfiguration)}. + */ + private WifiConfiguration getExistingConfiguredNetwork(String ssid) { + List<WifiConfiguration> wifiConfigs = mWifiManager.getConfiguredNetworks(); + if (wifiConfigs == null) { + return null; + } + for (WifiConfiguration config : wifiConfigs) { + if (config.SSID.equals(ssid)) { + return config; + } + } + return null; + } + /** + * Connect to a Wi-Fi network. + * + * @param wifiNetworkConfig A JSON object that contains the info required to connect to a Wi-Fi + * network. It follows the fields of WifiConfiguration type, e.g. {"SSID": "myWifi", + * "password": "12345678"}. + * @throws InterruptedException + * @throws JSONException + * @throws WifiManagerSnippetException + */ + @Rpc(description = "Connects to a Wi-Fi network.") + public void wifiConnect(JSONObject wifiNetworkConfig) + throws InterruptedException, JSONException, WifiManagerSnippetException { + Log.d("Got network config: " + wifiNetworkConfig); + WifiConfiguration wifiConfig = JsonDeserializer.jsonToWifiConfig(wifiNetworkConfig); + String SSID = wifiConfig.SSID; + // Return directly if network is already connected. + WifiInfo connectionInfo = mWifiManager.getConnectionInfo(); + if (connectionInfo.getNetworkId() != -1 + && connectionInfo.getSSID().equals(wifiConfig.SSID)) { + Log.d("Network " + connectionInfo.getSSID() + " is already connected."); + return; + } + int networkId; + // If this is a network with a known SSID, connect with the existing config. + // We have to do this because in N+, network configs can only be modified by the UID that + // created the network. So any attempt to modify a network config that does not belong to us + // would result in error. + WifiConfiguration existingConfig = getExistingConfiguredNetwork(wifiConfig.SSID); + if (existingConfig != null) { + Log.w( + "Connecting to network \"" + + existingConfig.SSID + + "\" with its existing configuration: " + + existingConfig.toString()); + wifiConfig = existingConfig; + networkId = wifiConfig.networkId; + } else { + // If this is a network with a new SSID, add the network. + networkId = mWifiManager.addNetwork(wifiConfig); + } + mWifiManager.disconnect(); + if (!mWifiManager.enableNetwork(networkId, true)) { + throw new WifiManagerSnippetException( + "Failed to enable Wi-Fi network of ID: " + networkId); + } + if (!mWifiManager.reconnect()) { + throw new WifiManagerSnippetException( + "Failed to reconnect to Wi-Fi network of ID: " + networkId); + } + if (!Utils.waitUntil( + () -> + mWifiManager.getConnectionInfo().getSSID().equals(SSID) + && mWifiManager.getConnectionInfo().getNetworkId() != -1 && mWifiManager + .getConnectionInfo().getSupplicantState().equals(SupplicantState.COMPLETED), + 90)) { + throw new WifiManagerSnippetException( + String.format( + "Failed to connect to '%s', timeout! Current connection: '%s'", + wifiNetworkConfig, mWifiManager.getConnectionInfo().getSSID())); + } + Log.d( + "Connected to network '" + + mWifiManager.getConnectionInfo().getSSID() + + "' with ID " + + mWifiManager.getConnectionInfo().getNetworkId()); + } + + @Rpc( + description = + "Forget a configured Wi-Fi network by its network ID, which is part of the" + + " WifiConfiguration.") + public void wifiRemoveNetwork(Integer networkId) throws WifiManagerSnippetException { + if (!mWifiManager.removeNetwork(networkId)) { + throw new WifiManagerSnippetException("Failed to remove network of ID: " + networkId); + } + } + + @Rpc( + description = + "Get the list of configured Wi-Fi networks, each is a serialized " + + "WifiConfiguration object.") + public List<JSONObject> wifiGetConfiguredNetworks() throws JSONException { + List<JSONObject> networks = new ArrayList<>(); + for (WifiConfiguration config : mWifiManager.getConfiguredNetworks()) { + networks.add(mJsonSerializer.toJson(config)); + } + return networks; + } + + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP) + @Rpc(description = "Enable or disable wifi verbose logging.") + public void wifiSetVerboseLogging(boolean enable) throws Throwable { + Utils.invokeByReflection(mWifiManager, "enableVerboseLogging", enable ? 1 : 0); + } + + @Rpc( + description = + "Get the information about the active Wi-Fi connection, which is a serialized " + + "WifiInfo object.") + public JSONObject wifiGetConnectionInfo() throws JSONException { + return mJsonSerializer.toJson(mWifiManager.getConnectionInfo()); + } + + @Rpc( + description = + "Get the info from last successful DHCP request, which is a serialized DhcpInfo " + + "object.") + public JSONObject wifiGetDhcpInfo() throws JSONException { + return mJsonSerializer.toJson(mWifiManager.getDhcpInfo()); + } + + @Rpc(description = "Check whether Wi-Fi Soft AP (hotspot) is enabled.") + public boolean wifiIsApEnabled() throws Throwable { + return (boolean) Utils.invokeByReflection(mWifiManager, "isWifiApEnabled"); + } + + @RequiresApi(Build.VERSION_CODES.LOLLIPOP) + @RpcMinSdk(Build.VERSION_CODES.LOLLIPOP) + @Rpc( + description = + "Check whether this device supports 5 GHz band Wi-Fi. " + + "Turn on Wi-Fi before calling.") + public boolean wifiIs5GHzBandSupported() { + return mWifiManager.is5GHzBandSupported(); + } + + /** + * Enable Wi-Fi Soft AP (hotspot). + * + * @param configuration The same format as the param wifiNetworkConfig param for wifiConnect. + * @throws Throwable + */ + @Rpc(description = "Enable Wi-Fi Soft AP (hotspot).") + public void wifiEnableSoftAp(@Nullable JSONObject configuration) throws Throwable { + // If no configuration is provided, the existing configuration would be used. + WifiConfiguration wifiConfiguration = null; + if (configuration != null) { + wifiConfiguration = JsonDeserializer.jsonToWifiConfig(configuration); + // Have to trim off the extra quotation marks since Soft AP logic interprets + // WifiConfiguration.SSID literally, unlike the WifiManager connection logic. + wifiConfiguration.SSID = JsonSerializer.trimQuotationMarks(wifiConfiguration.SSID); + } + if (!(boolean) + Utils.invokeByReflection( + mWifiManager, "setWifiApEnabled", wifiConfiguration, true)) { + throw new WifiManagerSnippetException("Failed to initiate turning on Wi-Fi Soft AP."); + } + if (!Utils.waitUntil(() -> wifiIsApEnabled() == true, 60)) { + throw new WifiManagerSnippetException( + "Timed out after 60s waiting for Wi-Fi Soft AP state to turn on with configuration: " + + configuration); + } + } + + /** Disables Wi-Fi Soft AP (hotspot). */ + @Rpc(description = "Disable Wi-Fi Soft AP (hotspot).") + public void wifiDisableSoftAp() throws Throwable { + if (!(boolean) + Utils.invokeByReflection( + mWifiManager, + "setWifiApEnabled", + null /* No configuration needed for disabling */, + false)) { + throw new WifiManagerSnippetException("Failed to initiate turning off Wi-Fi Soft AP."); + } + if (!Utils.waitUntil(() -> wifiIsApEnabled() == false, 60)) { + throw new WifiManagerSnippetException( + "Timed out after 60s waiting for Wi-Fi Soft AP state to turn off."); + } + } + + @Override + public void shutdown() {} + + /** + * Elevates permission as require for proper wifi controls. + * + * Starting in Android Q (29), additional restrictions are added for wifi operation. See + * below Android Q privacy changes for additional details. + * https://developer.android.com/preview/privacy/camera-connectivity + * + * @throws Throwable if failed to cleanup connection with UiAutomation + */ + private void adaptShellPermissionIfRequired() throws Throwable { + if (mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >= 29 + && Build.VERSION.SDK_INT >= 29) { + Log.d("Elevating permission require to enable support for wifi operation in Android Q+"); + UiAutomation uia = InstrumentationRegistry.getInstrumentation().getUiAutomation(); + uia.adoptShellPermissionIdentity(); + try { + Class<?> cls = Class.forName("android.app.UiAutomation"); + Method destroyMethod = cls.getDeclaredMethod("destroy"); + destroyMethod.invoke(uia); + } catch (NoSuchMethodException + | IllegalAccessException + | ClassNotFoundException + | InvocationTargetException e) { + throw new WifiManagerSnippetException("Failed to cleaup Ui Automation", e); + } + } + } + + private class WifiScanReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context c, Intent intent) { + String action = intent.getAction(); + if (action.equals(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)) { + mIsScanResultAvailable = true; + } + } + } +} |