1 /*
2  * Copyright (C) 2010 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of 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,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.tradefed.device;
17 
18 import com.android.ddmlib.MultiLineReceiver;
19 import com.android.tradefed.error.HarnessRuntimeException;
20 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
21 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
22 import com.android.tradefed.log.LogUtil.CLog;
23 import com.android.tradefed.result.error.DeviceErrorIdentifier;
24 import com.android.tradefed.util.CommandResult;
25 import com.android.tradefed.util.CommandStatus;
26 import com.android.tradefed.util.FileUtil;
27 import com.android.tradefed.util.IRunUtil;
28 import com.android.tradefed.util.RunUtil;
29 
30 import com.google.common.annotations.VisibleForTesting;
31 import com.google.common.base.Strings;
32 
33 import org.json.JSONException;
34 import org.json.JSONObject;
35 
36 import java.io.File;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.util.ArrayList;
40 import java.util.HashMap;
41 import java.util.Iterator;
42 import java.util.LinkedHashMap;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.concurrent.TimeUnit;
46 import java.util.regex.Matcher;
47 import java.util.regex.Pattern;
48 
49 /**
50  * Helper class for manipulating wifi services on device.
51  */
52 public class WifiHelper implements IWifiHelper {
53 
54     private static final String NULL = "null";
55     private static final String NULL_IP_ADDR = "0.0.0.0";
56     private static final String INSTRUMENTATION_CLASS = ".WifiUtil";
57     public static final String INSTRUMENTATION_PKG = "com.android.tradefed.utils.wifi";
58     static final String FULL_INSTRUMENTATION_NAME =
59             String.format("%s/%s", INSTRUMENTATION_PKG, INSTRUMENTATION_CLASS);
60 
61     static final String CHECK_PACKAGE_CMD =
62             String.format("dumpsys package %s", INSTRUMENTATION_PKG);
63     static final String ENABLE_WIFI_CMD = "svc wifi enable";
64     static final String DISABLE_WIFI_CMD = "svc wifi disable";
65     static final Pattern PACKAGE_VERSION_PAT = Pattern.compile("versionCode=(\\d*)");
66     static final int PACKAGE_VERSION_CODE = 21;
67 
68     private static final String WIFIUTIL_APK_NAME = "WifiUtil.apk";
69     private static final String WIFIUTIL_APK_RES = "/" + WIFIUTIL_APK_NAME;
70     private static final String WIFIUTIL_APK_RES_FALLBACK =
71             "/com/android/tradefed/utils/wifi/" + WIFIUTIL_APK_NAME;
72     /** the default WifiUtil command timeout in minutes */
73     private static final long WIFIUTIL_CMD_TIMEOUT_MINUTES = 5;
74 
75     /** the default time in ms to wait for a wifi state */
76     private static final long DEFAULT_WIFI_STATE_TIMEOUT = 200*1000;
77 
78     private final ITestDevice mDevice;
79     private File mWifiUtilApkFile;
80 
81     /** Tracks whether wifi helper needs to execute v2 operations. */
82     private boolean mUseV2;
83 
WifiHelper(ITestDevice device)84     public WifiHelper(ITestDevice device) throws DeviceNotAvailableException {
85         this(device, null, true);
86     }
87 
WifiHelper(ITestDevice device, String wifiUtilApkPath)88     public WifiHelper(ITestDevice device, String wifiUtilApkPath)
89             throws DeviceNotAvailableException {
90         this(device, wifiUtilApkPath, true);
91     }
92 
93     /** Alternative constructor that can skip the setup of the wifi apk. */
WifiHelper(ITestDevice device, String wifiUtilApkPath, boolean doSetup)94     public WifiHelper(ITestDevice device, String wifiUtilApkPath, boolean doSetup)
95             throws DeviceNotAvailableException {
96         this(device, wifiUtilApkPath, doSetup, false);
97     }
98 
99     /**
100      * Constructor to specify whether to use new wifi helper v2. v2 operations do not need to
101      * install the wifi util apk.
102      */
WifiHelper(ITestDevice device, String wifiUtilApkPath, boolean doSetup, boolean useV2)103     public WifiHelper(ITestDevice device, String wifiUtilApkPath, boolean doSetup, boolean useV2)
104             throws DeviceNotAvailableException {
105         mDevice = device;
106         mUseV2 = useV2;
107         if (doSetup) {
108             ensureDeviceSetup(wifiUtilApkPath);
109         }
110     }
111 
112     /**
113      * Get the {@link RunUtil} instance to use.
114      * <p/>
115      * Exposed for unit testing.
116      */
getRunUtil()117     IRunUtil getRunUtil() {
118         return RunUtil.getDefault();
119     }
120 
ensureDeviceSetup(String wifiUtilApkPath)121     void ensureDeviceSetup(String wifiUtilApkPath) throws DeviceNotAvailableException {
122         final String inst = mDevice.executeShellCommand(CHECK_PACKAGE_CMD);
123         if (inst != null) {
124             Matcher matcher = PACKAGE_VERSION_PAT.matcher(inst);
125             if (matcher.find()) {
126                 try {
127                     if (PACKAGE_VERSION_CODE <= Integer.parseInt(matcher.group(1))) {
128                         return;
129                     }
130                 } catch (NumberFormatException e) {
131                     CLog.w("failed to parse WifiUtil version code: %s", matcher.group(1));
132                 }
133             }
134         }
135 
136         // Attempt to install utility
137         try {
138             setupWifiUtilApkFile(wifiUtilApkPath);
139             String[] extraArgs = new String[] {};
140             if (mDevice.isBypassLowTargetSdkBlockSupported()) {
141                 extraArgs = new String[] {"--bypass-low-target-sdk-block"};
142             }
143 
144             final String error = mDevice.installPackage(mWifiUtilApkFile, true, extraArgs);
145             if (error == null) {
146                 // Installed successfully; good to go.
147                 return;
148             } else {
149                 if (error.contains("cmd: Failure calling service package")
150                     || error.contains("Can't find service: package")) {
151                     String message =
152                         String.format(
153                                 "Failed to install WifiUtil utility. Device might have"
154                                         + " crashed, it returned: %s", error);
155                     throw new DeviceRuntimeException(message, DeviceErrorIdentifier.DEVICE_CRASHED);
156                 }
157                 throw new HarnessRuntimeException(
158                         String.format(
159                                 "Unable to install WifiUtil utility: %s on %s",
160                                 error, mDevice.getSerialNumber()),
161                         DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
162             }
163         } catch (IOException e) {
164             throw new RuntimeException(String.format(
165                     "Failed to unpack WifiUtil utility: %s", e.getMessage()));
166         } finally {
167             // Delete the tmp file only if the APK is copied from classpath
168             if (wifiUtilApkPath == null) {
169                 FileUtil.deleteFile(mWifiUtilApkFile);
170             }
171         }
172     }
173 
setupWifiUtilApkFile(String wifiUtilApkPath)174     private void setupWifiUtilApkFile(String wifiUtilApkPath) throws IOException {
175         if (wifiUtilApkPath != null) {
176             mWifiUtilApkFile = new File(wifiUtilApkPath);
177         } else {
178             mWifiUtilApkFile = extractWifiUtilApk();
179         }
180     }
181 
182     /**
183      * Get the {@link File} object of the APK file.
184      *
185      * <p>Exposed for unit testing.
186      */
187     @VisibleForTesting
getWifiUtilApkFile()188     File getWifiUtilApkFile() {
189         return mWifiUtilApkFile;
190     }
191 
192     /**
193      * Helper method to extract the wifi util apk from the classpath
194      */
extractWifiUtilApk()195     public static File extractWifiUtilApk() throws IOException {
196         File apkTempFile = FileUtil.createTempFile(WIFIUTIL_APK_NAME, ".apk");
197         try {
198             InputStream apkStream = WifiHelper.class.getResourceAsStream(WIFIUTIL_APK_RES);
199             FileUtil.writeToFile(apkStream, apkTempFile);
200         } catch (IOException e) {
201             // Fallback to new path
202             InputStream apkStream = WifiHelper.class.getResourceAsStream(WIFIUTIL_APK_RES_FALLBACK);
203             FileUtil.writeToFile(apkStream, apkTempFile);
204         }
205         return apkTempFile;
206     }
207 
208     /**
209      * {@inheritDoc}
210      */
211     @Override
enableWifi()212     public boolean enableWifi() throws DeviceNotAvailableException {
213         if (mUseV2) {
214             return enableWifiV2();
215         }
216         CommandResult result = mDevice.executeShellV2Command(ENABLE_WIFI_CMD);
217         if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
218             CLog.e(
219                     "Failed to enable wifi. status: %s\nstdout: %s\nstderr: %s",
220                     result.getStatus(), result.getStdout(), result.getStderr());
221         }
222         // shell command does not produce any message to indicate success/failure, wait for state
223         // change to complete.
224         return waitForWifiEnabled(120000L);
225     }
226 
227     /** Uses the wifi cmd to enable wifi. */
enableWifiV2()228     private boolean enableWifiV2() throws DeviceNotAvailableException {
229         CommandResult enableOutput =
230                 mDevice.executeShellV2Command(
231                         String.format("cmd -w wifi set-wifi-enabled enabled"));
232         if (!CommandStatus.SUCCESS.equals(enableOutput.getStatus())) {
233             CLog.w(
234                     "Failed to enable wifi. stdout: %s\nstderr:%s",
235                     enableOutput.getStdout(), enableOutput.getStderr());
236             return false;
237         }
238         return waitForWifiEnabled(120000L);
239     }
240 
241     /**
242      * {@inheritDoc}
243      */
244     @Override
disableWifi()245     public boolean disableWifi() throws DeviceNotAvailableException {
246         mDevice.executeShellCommand(DISABLE_WIFI_CMD);
247         // shell command does not produce any message to indicate success/failure, wait for state
248         // change to complete.
249         return waitForWifiDisabled();
250     }
251 
252     /**
253      * {@inheritDoc}
254      */
255     @Override
waitForWifiState(WifiState... expectedStates)256     public boolean waitForWifiState(WifiState... expectedStates) throws DeviceNotAvailableException {
257         return waitForWifiState(DEFAULT_WIFI_STATE_TIMEOUT, expectedStates);
258     }
259 
260     /**
261      * Waits the given time until one of the expected wifi states occurs.
262      *
263      * @param expectedStates one or more wifi states to expect
264      * @param timeout max time in ms to wait
265      * @return <code>true</code> if the one of the expected states occurred. <code>false</code> if
266      *         none of the states occurred before timeout is reached
267      * @throws DeviceNotAvailableException
268      */
waitForWifiState(long timeout, WifiState... expectedStates)269      boolean waitForWifiState(long timeout, WifiState... expectedStates)
270             throws DeviceNotAvailableException {
271         long startTime = System.currentTimeMillis();
272         while (System.currentTimeMillis() < (startTime + timeout)) {
273             String state = runWifiUtil("getSupplicantState");
274             for (WifiState expectedState : expectedStates) {
275                 if (expectedState.name().equals(state)) {
276                     return true;
277                 }
278             }
279             getRunUtil().sleep(getPollTime());
280         }
281         return false;
282     }
283 
284     /**
285      * Gets the time to sleep between poll attempts
286      */
getPollTime()287     long getPollTime() {
288         return 1*1000;
289     }
290 
291     /**
292      * Remove the network identified by an integer network id.
293      *
294      * @param networkId the network id identifying its profile in wpa_supplicant configuration
295      * @throws DeviceNotAvailableException
296      */
removeNetwork(int networkId)297     boolean removeNetwork(int networkId) throws DeviceNotAvailableException {
298         if (!asBool(runWifiUtil("removeNetwork", "id", Integer.toString(networkId)))) {
299             return false;
300         }
301         if (!asBool(runWifiUtil("saveConfiguration"))) {
302             return false;
303         }
304         return true;
305     }
306 
307     /**
308      * {@inheritDoc}
309      */
310     @Override
addOpenNetwork(String ssid)311     public boolean addOpenNetwork(String ssid) throws DeviceNotAvailableException {
312         return addOpenNetwork(ssid, false);
313     }
314 
315     /**
316      * {@inheritDoc}
317      */
318     @Override
addOpenNetwork(String ssid, boolean scanSsid)319     public boolean addOpenNetwork(String ssid, boolean scanSsid)
320             throws DeviceNotAvailableException {
321         int id = asInt(runWifiUtil("addOpenNetwork", "ssid", ssid, "scanSsid",
322                 Boolean.toString(scanSsid)));
323         if (id < 0) {
324             return false;
325         }
326         if (!asBool(runWifiUtil("associateNetwork", "id", Integer.toString(id)))) {
327             return false;
328         }
329         if (!asBool(runWifiUtil("saveConfiguration"))) {
330             return false;
331         }
332         return true;
333     }
334 
335     /**
336      * {@inheritDoc}
337      */
338     @Override
addWpaPskNetwork(String ssid, String psk)339     public boolean addWpaPskNetwork(String ssid, String psk) throws DeviceNotAvailableException {
340         return addWpaPskNetwork(ssid, psk, false);
341     }
342 
343     /**
344      * {@inheritDoc}
345      */
346     @Override
addWpaPskNetwork(String ssid, String psk, boolean scanSsid)347     public boolean addWpaPskNetwork(String ssid, String psk, boolean scanSsid)
348             throws DeviceNotAvailableException {
349         int id = asInt(runWifiUtil("addWpaPskNetwork", "ssid", ssid, "psk", psk, "scan_ssid",
350                 Boolean.toString(scanSsid)));
351         if (id < 0) {
352             return false;
353         }
354         if (!asBool(runWifiUtil("associateNetwork", "id", Integer.toString(id)))) {
355             return false;
356         }
357         if (!asBool(runWifiUtil("saveConfiguration"))) {
358             return false;
359         }
360         return true;
361     }
362 
363     /**
364      * {@inheritDoc}
365      */
366     @Override
waitForIp(long timeout)367     public boolean waitForIp(long timeout) throws DeviceNotAvailableException {
368         long startTime = System.currentTimeMillis();
369 
370         while (System.currentTimeMillis() < (startTime + timeout)) {
371             if (hasValidIp()) {
372                 return true;
373             }
374             getRunUtil().sleep(getPollTime());
375         }
376         return false;
377     }
378 
379     /**
380      * {@inheritDoc}
381      */
382     @Override
hasValidIp()383     public boolean hasValidIp() throws DeviceNotAvailableException {
384         final String ip = getIpAddress();
385         return ip != null && !ip.isEmpty() && !NULL_IP_ADDR.equals(ip);
386     }
387 
388     /**
389      * {@inheritDoc}
390      */
391     @Override
getIpAddress()392     public String getIpAddress() throws DeviceNotAvailableException {
393         return runWifiUtil("getIpAddress");
394     }
395 
396     /**
397      * {@inheritDoc}
398      */
399     @Override
getSSID()400     public String getSSID() throws DeviceNotAvailableException {
401         return runWifiUtil("getSSID");
402     }
403 
404     /**
405      * {@inheritDoc}
406      */
407     @Override
getBSSID()408     public String getBSSID() throws DeviceNotAvailableException {
409         return runWifiUtil("getBSSID");
410     }
411 
412     /**
413      * {@inheritDoc}
414      */
415     @Override
removeAllNetworks()416     public boolean removeAllNetworks() throws DeviceNotAvailableException {
417         if (!asBool(runWifiUtil("removeAllNetworks"))) {
418             return false;
419         }
420         if (!asBool(runWifiUtil("saveConfiguration"))) {
421             return false;
422         }
423         return true;
424     }
425 
426     /**
427      * {@inheritDoc}
428      */
429     @Override
isWifiEnabled()430     public boolean isWifiEnabled() throws DeviceNotAvailableException {
431         if (mUseV2) {
432             return isWifiEnabledV2();
433         } else {
434             return asBool(
435                     runWifiUtil(
436                             "isWifiEnabled", 2
437                             /** 2 minutes timeout */
438                             ));
439         }
440     }
441 
isWifiEnabledV2()442     private boolean isWifiEnabledV2() throws DeviceNotAvailableException {
443         CommandResult statusOutput =
444                 mDevice.executeShellV2Command(String.format("cmd -w wifi status"));
445         if (CommandStatus.SUCCESS.equals(statusOutput.getStatus())
446                 && !statusOutput.getStdout().contains("Wifi is disabled")) {
447             return true;
448         } else {
449             return false;
450         }
451     }
452 
453     /**
454      * {@inheritDoc}
455      */
456     @Override
waitForWifiEnabled()457     public boolean waitForWifiEnabled() throws DeviceNotAvailableException {
458         return waitForWifiEnabled(DEFAULT_WIFI_STATE_TIMEOUT);
459     }
460 
461     @Override
waitForWifiEnabled(long timeout)462     public boolean waitForWifiEnabled(long timeout) throws DeviceNotAvailableException {
463         long startTime = System.currentTimeMillis();
464 
465         while (System.currentTimeMillis() < (startTime + timeout)) {
466             if (isWifiEnabled()) {
467                 return true;
468             }
469             getRunUtil().sleep(getPollTime());
470         }
471         return false;
472     }
473 
474     /**
475      * {@inheritDoc}
476      */
477     @Override
waitForWifiDisabled()478     public boolean waitForWifiDisabled() throws DeviceNotAvailableException {
479         return waitForWifiDisabled(DEFAULT_WIFI_STATE_TIMEOUT);
480     }
481 
482     @Override
waitForWifiDisabled(long timeout)483     public boolean waitForWifiDisabled(long timeout) throws DeviceNotAvailableException {
484         long startTime = System.currentTimeMillis();
485 
486         while (System.currentTimeMillis() < (startTime + timeout)) {
487             if (!isWifiEnabled()) {
488                 return true;
489             }
490             getRunUtil().sleep(getPollTime());
491         }
492         return false;
493     }
494 
495     /**
496      * {@inheritDoc}
497      */
498     @Override
getWifiInfo()499     public Map<String, String> getWifiInfo() throws DeviceNotAvailableException {
500         if (mUseV2) {
501             return getWifiInfoV2();
502         }
503 
504         Map<String, String> info = new HashMap<>();
505 
506         final String result = runWifiUtil("getWifiInfo");
507         if (result != null) {
508             try {
509                 final JSONObject json = new JSONObject(result);
510                 final Iterator<?> keys = json.keys();
511                 while (keys.hasNext()) {
512                     final String key = (String)keys.next();
513                     info.put(key, json.getString(key));
514                 }
515             } catch(final JSONException e) {
516                 CLog.w("Failed to parse wifi info: %s", e.getMessage());
517             }
518         }
519 
520         return info;
521     }
522 
getWifiInfoV2()523     private Map<String, String> getWifiInfoV2() throws DeviceNotAvailableException {
524         CommandResult statusOutput =
525                 mDevice.executeShellV2Command(String.format("cmd -w wifi status"));
526         if (CommandStatus.SUCCESS.equals(statusOutput.getStatus())) {
527             return WifiCommandUtil.parseWifiInfo(statusOutput.getStdout());
528         } else {
529             return new LinkedHashMap<>();
530         }
531     }
532 
533     /**
534      * {@inheritDoc}
535      */
536     @Override
checkConnectivity(String urlToCheck)537     public boolean checkConnectivity(String urlToCheck) throws DeviceNotAvailableException {
538         if (mUseV2) {
539             return checkConnectivityV2(120000L);
540         }
541         return asBool(runWifiUtil("checkConnectivity", "urlToCheck", urlToCheck));
542     }
543 
checkConnectivityV2(long timeout)544     private boolean checkConnectivityV2(long timeout) throws DeviceNotAvailableException {
545         long startTime = System.currentTimeMillis();
546         while (System.currentTimeMillis() < (startTime + timeout)) {
547             CommandResult statusOutput =
548                     mDevice.executeShellV2Command(String.format("cmd -w wifi status"));
549             if (CommandStatus.SUCCESS.equals(statusOutput.getStatus())
550                     && statusOutput.getStdout().contains("Wifi is connected")) {
551                 return true;
552             }
553             getRunUtil().sleep(getPollTime());
554         }
555         CLog.w("Wifi is not connected after timeout: %d ms.", timeout);
556         return false;
557     }
558 
559     /**
560      * {@inheritDoc}
561      */
562     @Override
connectToNetwork(String ssid, String psk, String urlToCheck)563     public boolean connectToNetwork(String ssid, String psk, String urlToCheck)
564             throws DeviceNotAvailableException {
565         return WifiConnectionResult.SUCCESS == connectToNetwork(ssid, psk, urlToCheck, false);
566     }
567 
568     /** {@inheritDoc} */
569     @Override
connectToNetwork( String ssid, String psk, String urlToCheck, boolean scanSsid)570     public WifiConnectionResult connectToNetwork(
571             String ssid, String psk, String urlToCheck, boolean scanSsid)
572             throws DeviceNotAvailableException {
573         return connectToNetwork(ssid, psk, urlToCheck, scanSsid, null);
574     }
575     /** {@inheritDoc} */
576     @Override
connectToNetwork( String ssid, String psk, String urlToCheck, boolean scanSsid, String defaultType)577     public WifiConnectionResult connectToNetwork(
578             String ssid, String psk, String urlToCheck, boolean scanSsid, String defaultType)
579             throws DeviceNotAvailableException {
580         if (mUseV2) {
581             return connectToNetworkV2(ssid, psk, scanSsid, defaultType);
582         }
583 
584         if (!enableWifi()) {
585             CLog.e("Failed to enable wifi");
586             return WifiConnectionResult.FAILED_TO_ENABLE;
587         }
588         if (!asBool(runWifiUtil("connectToNetwork", "ssid", ssid, "psk", psk, "urlToCheck",
589                 urlToCheck, "scan_ssid", Boolean.toString(scanSsid)))) {
590             return WifiConnectionResult.FAILED_TO_CONNECT;
591         }
592         return WifiConnectionResult.SUCCESS;
593     }
594 
595     /** Uses the wifi connect-network cmd to connect to wifi network instead of using the apk. */
connectToNetworkV2( String ssid, String psk, boolean scanSsid, String defaultType)596     private WifiConnectionResult connectToNetworkV2(
597             String ssid, String psk, boolean scanSsid, String defaultType)
598             throws DeviceNotAvailableException {
599         if (Strings.isNullOrEmpty(ssid)) {
600             CLog.d("SSID of the wifi network can not be null or empty.");
601             return WifiConnectionResult.FAILED_TO_CONNECT;
602         }
603         if (psk == null) {
604             // psk can be empty for open networks.
605             psk = "";
606         }
607         if (!enableWifiV2()) {
608             return WifiConnectionResult.FAILED_TO_ENABLE;
609         }
610         // after enabling wifi, wait for scan results to appear
611         List<WifiCommandUtil.ScanResult> scanResults = getScanResults(120000L);
612         if (scanResults == null) {
613             return WifiConnectionResult.FAILED_TO_CONNECT;
614         }
615         String networkType = findNetworkType(ssid, scanResults);
616         if (networkType == null) {
617             if (defaultType != null) {
618                 // Use the default network type if we fail to find it.
619                 CLog.d(
620                         "Defaulting to a `%s` network type. Network connection may fail.",
621                         defaultType);
622                 networkType = defaultType;
623             } else {
624                 return WifiConnectionResult.FAILED_TO_CONNECT;
625             }
626         }
627         String connectCmd =
628                 String.format(
629                         "cmd -w wifi connect-network %s %s %s",
630                         quote(ssid), networkType, quote(psk));
631         if (scanSsid) {
632             connectCmd += " -h";
633         }
634         CommandResult connectOutput = mDevice.executeShellV2Command(connectCmd);
635         if (CommandStatus.SUCCESS.equals(connectOutput.getStatus())
636                 && checkConnectivityV2(120000L)) {
637             CLog.i("Successfully connected to wifi network %s", ssid);
638             InvocationMetricLogger.addInvocationMetrics(InvocationMetricKey.WIFI_AP_NAME, ssid);
639             return WifiConnectionResult.SUCCESS;
640         } else {
641             CLog.w(
642                     "Failed to connect to wifi. stdout: %s\nstderr:%s",
643                     connectOutput.getStdout(), connectOutput.getStderr());
644             return WifiConnectionResult.FAILED_TO_CONNECT;
645         }
646     }
647 
648     /** Returns a list of scan result using the `wifi list-scan-results` command. */
getScanResults(long timeout)649     private List<WifiCommandUtil.ScanResult> getScanResults(long timeout)
650             throws DeviceNotAvailableException {
651         // start a new scan
652         mDevice.executeShellV2Command("cmd -w wifi start-scan");
653         // we might need to wait for scan results to be available.
654         long startTime = System.currentTimeMillis();
655         while (System.currentTimeMillis() < (startTime + timeout)) {
656             CommandResult listOutput =
657                     mDevice.executeShellV2Command(String.format("cmd -w wifi list-scan-results"));
658             if (!CommandStatus.SUCCESS.equals(listOutput.getStatus())) {
659                 CLog.w(
660                         "Failed to list wifi scan results. stdout: %s\nstderr:%s",
661                         listOutput.getStdout(), listOutput.getStderr());
662                 return null;
663             }
664 
665             List<WifiCommandUtil.ScanResult> scanResults =
666                     WifiCommandUtil.parseScanResults(listOutput.getStdout());
667             if (!scanResults.isEmpty()) {
668                 return scanResults;
669             }
670             CLog.d("Scan results is not available yet. Scan Results:\n%s", listOutput.getStdout());
671             getRunUtil().sleep(getPollTime());
672         }
673         CLog.d("Failed to find scan results after timeout: %d ms", timeout);
674         return null;
675     }
676 
677     /** Returns the network type for a given Ssid. */
findNetworkType(String ssid, List<WifiCommandUtil.ScanResult> scanResults)678     private String findNetworkType(String ssid, List<WifiCommandUtil.ScanResult> scanResults) {
679         // Find the scan result for the ssid
680         for (WifiCommandUtil.ScanResult scanResult : scanResults) {
681             if (scanResult.getInfo("SSID").equals(ssid)) {
682                 return WifiCommandUtil.resolveNetworkType(scanResult.getInfo("Flags"));
683             }
684         }
685         CLog.w("Failed to find scan result for ssid %s from scanResults: \n%s", ssid, scanResults);
686         return null;
687     }
688 
689     /**
690      * {@inheritDoc}
691      */
692     @Override
disconnectFromNetwork()693     public boolean disconnectFromNetwork() throws DeviceNotAvailableException {
694         if (!asBool(runWifiUtil("disconnectFromNetwork"))) {
695             return false;
696         }
697         if (!disableWifi()) {
698             CLog.e("Failed to disable wifi");
699             return false;
700         }
701         return true;
702     }
703 
704     /**
705      * {@inheritDoc}
706      */
707     @Override
startMonitor(long interval, String urlToCheck)708     public boolean startMonitor(long interval, String urlToCheck) throws DeviceNotAvailableException {
709         return asBool(runWifiUtil("startMonitor", "interval", Long.toString(interval), "urlToCheck",
710                 urlToCheck));
711     }
712 
713     /**
714      * {@inheritDoc}
715      */
716     @Override
stopMonitor()717     public List<Long> stopMonitor() throws DeviceNotAvailableException {
718         final String output = runWifiUtil("stopMonitor");
719         if (output == null || output.isEmpty() || NULL.equals(output)) {
720             return new ArrayList<Long>(0);
721         }
722 
723         String[] tokens = output.split(",");
724         List<Long> values = new ArrayList<Long>(tokens.length);
725         for (final String token : tokens) {
726             values.add(Long.parseLong(token));
727         }
728         return values;
729     }
730 
runWifiUtil(String method, String... args)731     private String runWifiUtil(String method, String... args) throws DeviceNotAvailableException {
732         return runWifiUtil(method, WIFIUTIL_CMD_TIMEOUT_MINUTES, args);
733     }
734 
735     /**
736      * Run a WifiUtil command and return the result
737      *
738      * @param method the WifiUtil method to call
739      * @param timeout in minutes for the command
740      * @param args a flat list of [arg-name, value] pairs to pass
741      * @return The value of the result field in the output, or <code>null</code> if result could not
742      *     be parsed
743      */
runWifiUtil(String method, long timeout, String... args)744     private String runWifiUtil(String method, long timeout, String... args)
745             throws DeviceNotAvailableException {
746         final String cmd = buildWifiUtilCmd(method, args);
747 
748         WifiUtilOutput parser = new WifiUtilOutput();
749         mDevice.executeShellCommand(cmd, parser, timeout, timeout, TimeUnit.MINUTES, 0);
750         if (parser.getError() != null) {
751             String errorMessage =
752                     String.format(
753                             "Failed to %s due to: '%s'. See logcat for details.",
754                             method, parser.getError());
755             CLog.e(errorMessage);
756         }
757         return parser.getResult();
758     }
759 
760     /**
761      * Build and return a WifiUtil command for the specified method and args
762      *
763      * @param method the WifiUtil method to call
764      * @param args a flat list of [arg-name, value] pairs to pass
765      * @return the command to be executed on the device shell
766      */
buildWifiUtilCmd(String method, String... args)767     static String buildWifiUtilCmd(String method, String... args) {
768         Map<String, String> argMap = new HashMap<String, String>();
769         argMap.put("method", method);
770         if ((args.length & 0x1) == 0x1) {
771             throw new IllegalArgumentException(
772                     "args should have even length, consisting of key and value pairs");
773         }
774         for (int i = 0; i < args.length; i += 2) {
775             // Skip null parameters
776             if (args[i+1] == null) {
777                 continue;
778             }
779             argMap.put(args[i], args[i+1]);
780         }
781         return buildWifiUtilCmdFromMap(argMap);
782     }
783 
784     /**
785      * Build and return a WifiUtil command for the specified args
786      *
787      * @param args A Map of (arg-name, value) pairs to pass as "-e" arguments to the `am` command
788      * @return the commadn to be executed on the device shell
789      */
buildWifiUtilCmdFromMap(Map<String, String> args)790     static String buildWifiUtilCmdFromMap(Map<String, String> args) {
791         StringBuilder sb = new StringBuilder("am instrument");
792 
793         for (Map.Entry<String, String> arg : args.entrySet()) {
794             sb.append(" -e ");
795             sb.append(arg.getKey());
796             sb.append(" ");
797             sb.append(quote(arg.getValue()));
798         }
799 
800         sb.append(" -w ");
801         sb.append(INSTRUMENTATION_PKG);
802         sb.append("/");
803         sb.append(INSTRUMENTATION_CLASS);
804 
805         return sb.toString();
806     }
807 
808     /**
809      * Helper function to convert a String to an Integer
810      */
asInt(String str)811     private static int asInt(String str) {
812         if (str == null) {
813             return -1;
814         }
815         try {
816             return Integer.parseInt(str);
817         } catch (NumberFormatException e) {
818             return -1;
819         }
820     }
821 
822     /**
823      * Helper function to convert a String to a boolean.  Maps "true" to true, and everything else
824      * to false.
825      */
asBool(String str)826     private static boolean asBool(String str) {
827         return "true".equals(str);
828     }
829 
830     /**
831      * Helper function to wrap the specified String in single-quotes to prevent shell interpretation
832      */
quote(String str)833     private static String quote(String str) {
834         return "'" + str.replace("'", "'\\''") + "'";
835     }
836 
837     /**
838      * Processes the output of a WifiUtil invocation
839      */
840     private static class WifiUtilOutput extends MultiLineReceiver {
841         private static final Pattern RESULT_PAT =
842                 Pattern.compile("INSTRUMENTATION_RESULT: result=(.*)");
843         private static final Pattern ERROR_PAT =
844                 Pattern.compile("INSTRUMENTATION_RESULT: error=(.*)");
845 
846         private String mResult = null;
847         private String mError = null;
848 
849         /**
850          * {@inheritDoc}
851          */
852         @Override
processNewLines(String[] lines)853         public void processNewLines(String[] lines) {
854             for (String line : lines) {
855                 Matcher resultMatcher = RESULT_PAT.matcher(line);
856                 if (resultMatcher.matches()) {
857                     mResult = resultMatcher.group(1);
858                     continue;
859                 }
860 
861                 Matcher errorMatcher = ERROR_PAT.matcher(line);
862                 if (errorMatcher.matches()) {
863                     mError = errorMatcher.group(1);
864                 }
865             }
866         }
867 
868         /**
869          * Return the result flag parsed from instrumentation output. <code>null</code> is returned
870          * if result output was not present.
871          */
getResult()872         String getResult() {
873             return mResult;
874         }
875 
getError()876         String getError() {
877             return mError;
878         }
879 
880         /**
881          * {@inheritDoc}
882          */
883         @Override
isCancelled()884         public boolean isCancelled() {
885             return false;
886         }
887     }
888 
889     /** {@inheritDoc} */
890     @Override
cleanUp()891     public void cleanUp() throws DeviceNotAvailableException {
892         String output = mDevice.uninstallPackage(INSTRUMENTATION_PKG);
893         if (output != null) {
894             CLog.w("Error '%s' occurred when uninstalling %s", output, INSTRUMENTATION_PKG);
895         } else {
896             CLog.d("Successfully clean up WifiHelper.");
897         }
898     }
899 }
900 
901