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