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.log.LogUtil.CLog;
21 import com.android.tradefed.result.error.DeviceErrorIdentifier;
22 import com.android.tradefed.util.FileUtil;
23 import com.android.tradefed.util.IRunUtil;
24 import com.android.tradefed.util.RunUtil;
25 
26 import com.google.common.annotations.VisibleForTesting;
27 
28 import org.json.JSONException;
29 import org.json.JSONObject;
30 
31 import java.io.File;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.util.ArrayList;
35 import java.util.HashMap;
36 import java.util.Iterator;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.concurrent.TimeUnit;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42 
43 /**
44  * Helper class for manipulating wifi services on device.
45  */
46 public class WifiHelper implements IWifiHelper {
47 
48     private static final String NULL = "null";
49     private static final String NULL_IP_ADDR = "0.0.0.0";
50     private static final String INSTRUMENTATION_CLASS = ".WifiUtil";
51     public static final String INSTRUMENTATION_PKG = "com.android.tradefed.utils.wifi";
52     static final String FULL_INSTRUMENTATION_NAME =
53             String.format("%s/%s", INSTRUMENTATION_PKG, INSTRUMENTATION_CLASS);
54 
55     static final String CHECK_PACKAGE_CMD =
56             String.format("dumpsys package %s", INSTRUMENTATION_PKG);
57     static final String ENABLE_WIFI_CMD = "svc wifi enable";
58     static final String DISABLE_WIFI_CMD = "svc wifi disable";
59     static final Pattern PACKAGE_VERSION_PAT = Pattern.compile("versionCode=(\\d*)");
60     static final int PACKAGE_VERSION_CODE = 21;
61 
62     private static final String WIFIUTIL_APK_NAME = "WifiUtil.apk";
63     /** the default WifiUtil command timeout in minutes */
64     private static final long WIFIUTIL_CMD_TIMEOUT_MINUTES = 5;
65 
66     /** the default time in ms to wait for a wifi state */
67     private static final long DEFAULT_WIFI_STATE_TIMEOUT = 200*1000;
68 
69     private final ITestDevice mDevice;
70     private File mWifiUtilApkFile;
71 
WifiHelper(ITestDevice device)72     public WifiHelper(ITestDevice device) throws DeviceNotAvailableException {
73         this(device, null, true);
74     }
75 
WifiHelper(ITestDevice device, String wifiUtilApkPath)76     public WifiHelper(ITestDevice device, String wifiUtilApkPath)
77             throws DeviceNotAvailableException {
78         this(device, wifiUtilApkPath, true);
79     }
80 
81     /** Alternative constructor that can skip the setup of the wifi apk. */
WifiHelper(ITestDevice device, String wifiUtilApkPath, boolean doSetup)82     public WifiHelper(ITestDevice device, String wifiUtilApkPath, boolean doSetup)
83             throws DeviceNotAvailableException {
84         mDevice = device;
85         if (doSetup) {
86             ensureDeviceSetup(wifiUtilApkPath);
87         }
88     }
89 
90     /**
91      * Get the {@link RunUtil} instance to use.
92      * <p/>
93      * Exposed for unit testing.
94      */
getRunUtil()95     IRunUtil getRunUtil() {
96         return RunUtil.getDefault();
97     }
98 
ensureDeviceSetup(String wifiUtilApkPath)99     void ensureDeviceSetup(String wifiUtilApkPath) throws DeviceNotAvailableException {
100         final String inst = mDevice.executeShellCommand(CHECK_PACKAGE_CMD);
101         if (inst != null) {
102             Matcher matcher = PACKAGE_VERSION_PAT.matcher(inst);
103             if (matcher.find()) {
104                 try {
105                     if (PACKAGE_VERSION_CODE <= Integer.parseInt(matcher.group(1))) {
106                         return;
107                     }
108                 } catch (NumberFormatException e) {
109                     CLog.w("failed to parse WifiUtil version code: %s", matcher.group(1));
110                 }
111             }
112         }
113 
114         // Attempt to install utility
115         try {
116             setupWifiUtilApkFile(wifiUtilApkPath);
117 
118             final String error = mDevice.installPackage(mWifiUtilApkFile, true);
119             if (error == null) {
120                 // Installed successfully; good to go.
121                 return;
122             } else {
123                 throw new HarnessRuntimeException(
124                         String.format(
125                                 "Unable to install WifiUtil utility: %s on %s",
126                                 error, mDevice.getSerialNumber()),
127                         DeviceErrorIdentifier.APK_INSTALLATION_FAILED);
128             }
129         } catch (IOException e) {
130             throw new RuntimeException(String.format(
131                     "Failed to unpack WifiUtil utility: %s", e.getMessage()));
132         } finally {
133             // Delete the tmp file only if the APK is copied from classpath
134             if (wifiUtilApkPath == null) {
135                 FileUtil.deleteFile(mWifiUtilApkFile);
136             }
137         }
138     }
139 
setupWifiUtilApkFile(String wifiUtilApkPath)140     private void setupWifiUtilApkFile(String wifiUtilApkPath) throws IOException {
141         if (wifiUtilApkPath != null) {
142             mWifiUtilApkFile = new File(wifiUtilApkPath);
143         } else {
144             mWifiUtilApkFile = extractWifiUtilApk();
145         }
146     }
147 
148     /**
149      * Get the {@link File} object of the APK file.
150      *
151      * <p>Exposed for unit testing.
152      */
153     @VisibleForTesting
getWifiUtilApkFile()154     File getWifiUtilApkFile() {
155         return mWifiUtilApkFile;
156     }
157 
158     /**
159      * Helper method to extract the wifi util apk from the classpath
160      */
extractWifiUtilApk()161     public static File extractWifiUtilApk() throws IOException {
162         File apkTempFile;
163         apkTempFile = FileUtil.createTempFile(WIFIUTIL_APK_NAME, ".apk");
164         InputStream apkStream = WifiHelper.class.getResourceAsStream(
165             String.format("/apks/wifiutil/%s", WIFIUTIL_APK_NAME));
166         FileUtil.writeToFile(apkStream, apkTempFile);
167         return apkTempFile;
168     }
169 
170     /**
171      * {@inheritDoc}
172      */
173     @Override
enableWifi()174     public boolean enableWifi() throws DeviceNotAvailableException {
175         mDevice.executeShellCommand(ENABLE_WIFI_CMD);
176         // shell command does not produce any message to indicate success/failure, wait for state
177         // change to complete.
178         return waitForWifiEnabled();
179     }
180 
181     /**
182      * {@inheritDoc}
183      */
184     @Override
disableWifi()185     public boolean disableWifi() throws DeviceNotAvailableException {
186         mDevice.executeShellCommand(DISABLE_WIFI_CMD);
187         // shell command does not produce any message to indicate success/failure, wait for state
188         // change to complete.
189         return waitForWifiDisabled();
190     }
191 
192     /**
193      * {@inheritDoc}
194      */
195     @Override
waitForWifiState(WifiState... expectedStates)196     public boolean waitForWifiState(WifiState... expectedStates) throws DeviceNotAvailableException {
197         return waitForWifiState(DEFAULT_WIFI_STATE_TIMEOUT, expectedStates);
198     }
199 
200     /**
201      * Waits the given time until one of the expected wifi states occurs.
202      *
203      * @param expectedStates one or more wifi states to expect
204      * @param timeout max time in ms to wait
205      * @return <code>true</code> if the one of the expected states occurred. <code>false</code> if
206      *         none of the states occurred before timeout is reached
207      * @throws DeviceNotAvailableException
208      */
waitForWifiState(long timeout, WifiState... expectedStates)209      boolean waitForWifiState(long timeout, WifiState... expectedStates)
210             throws DeviceNotAvailableException {
211         long startTime = System.currentTimeMillis();
212         while (System.currentTimeMillis() < (startTime + timeout)) {
213             String state = runWifiUtil("getSupplicantState");
214             for (WifiState expectedState : expectedStates) {
215                 if (expectedState.name().equals(state)) {
216                     return true;
217                 }
218             }
219             getRunUtil().sleep(getPollTime());
220         }
221         return false;
222     }
223 
224     /**
225      * Gets the time to sleep between poll attempts
226      */
getPollTime()227     long getPollTime() {
228         return 1*1000;
229     }
230 
231     /**
232      * Remove the network identified by an integer network id.
233      *
234      * @param networkId the network id identifying its profile in wpa_supplicant configuration
235      * @throws DeviceNotAvailableException
236      */
removeNetwork(int networkId)237     boolean removeNetwork(int networkId) throws DeviceNotAvailableException {
238         if (!asBool(runWifiUtil("removeNetwork", "id", Integer.toString(networkId)))) {
239             return false;
240         }
241         if (!asBool(runWifiUtil("saveConfiguration"))) {
242             return false;
243         }
244         return true;
245     }
246 
247     /**
248      * {@inheritDoc}
249      */
250     @Override
addOpenNetwork(String ssid)251     public boolean addOpenNetwork(String ssid) throws DeviceNotAvailableException {
252         return addOpenNetwork(ssid, false);
253     }
254 
255     /**
256      * {@inheritDoc}
257      */
258     @Override
addOpenNetwork(String ssid, boolean scanSsid)259     public boolean addOpenNetwork(String ssid, boolean scanSsid)
260             throws DeviceNotAvailableException {
261         int id = asInt(runWifiUtil("addOpenNetwork", "ssid", ssid, "scanSsid",
262                 Boolean.toString(scanSsid)));
263         if (id < 0) {
264             return false;
265         }
266         if (!asBool(runWifiUtil("associateNetwork", "id", Integer.toString(id)))) {
267             return false;
268         }
269         if (!asBool(runWifiUtil("saveConfiguration"))) {
270             return false;
271         }
272         return true;
273     }
274 
275     /**
276      * {@inheritDoc}
277      */
278     @Override
addWpaPskNetwork(String ssid, String psk)279     public boolean addWpaPskNetwork(String ssid, String psk) throws DeviceNotAvailableException {
280         return addWpaPskNetwork(ssid, psk, false);
281     }
282 
283     /**
284      * {@inheritDoc}
285      */
286     @Override
addWpaPskNetwork(String ssid, String psk, boolean scanSsid)287     public boolean addWpaPskNetwork(String ssid, String psk, boolean scanSsid)
288             throws DeviceNotAvailableException {
289         int id = asInt(runWifiUtil("addWpaPskNetwork", "ssid", ssid, "psk", psk, "scan_ssid",
290                 Boolean.toString(scanSsid)));
291         if (id < 0) {
292             return false;
293         }
294         if (!asBool(runWifiUtil("associateNetwork", "id", Integer.toString(id)))) {
295             return false;
296         }
297         if (!asBool(runWifiUtil("saveConfiguration"))) {
298             return false;
299         }
300         return true;
301     }
302 
303     /**
304      * {@inheritDoc}
305      */
306     @Override
waitForIp(long timeout)307     public boolean waitForIp(long timeout) throws DeviceNotAvailableException {
308         long startTime = System.currentTimeMillis();
309 
310         while (System.currentTimeMillis() < (startTime + timeout)) {
311             if (hasValidIp()) {
312                 return true;
313             }
314             getRunUtil().sleep(getPollTime());
315         }
316         return false;
317     }
318 
319     /**
320      * {@inheritDoc}
321      */
322     @Override
hasValidIp()323     public boolean hasValidIp() throws DeviceNotAvailableException {
324         final String ip = getIpAddress();
325         return ip != null && !ip.isEmpty() && !NULL_IP_ADDR.equals(ip);
326     }
327 
328     /**
329      * {@inheritDoc}
330      */
331     @Override
getIpAddress()332     public String getIpAddress() throws DeviceNotAvailableException {
333         return runWifiUtil("getIpAddress");
334     }
335 
336     /**
337      * {@inheritDoc}
338      */
339     @Override
getSSID()340     public String getSSID() throws DeviceNotAvailableException {
341         return runWifiUtil("getSSID");
342     }
343 
344     /**
345      * {@inheritDoc}
346      */
347     @Override
getBSSID()348     public String getBSSID() throws DeviceNotAvailableException {
349         return runWifiUtil("getBSSID");
350     }
351 
352     /**
353      * {@inheritDoc}
354      */
355     @Override
removeAllNetworks()356     public boolean removeAllNetworks() throws DeviceNotAvailableException {
357         if (!asBool(runWifiUtil("removeAllNetworks"))) {
358             return false;
359         }
360         if (!asBool(runWifiUtil("saveConfiguration"))) {
361             return false;
362         }
363         return true;
364     }
365 
366     /**
367      * {@inheritDoc}
368      */
369     @Override
isWifiEnabled()370     public boolean isWifiEnabled() throws DeviceNotAvailableException {
371         return asBool(runWifiUtil("isWifiEnabled"));
372     }
373 
374     /**
375      * {@inheritDoc}
376      */
377     @Override
waitForWifiEnabled()378     public boolean waitForWifiEnabled() throws DeviceNotAvailableException {
379         return waitForWifiEnabled(DEFAULT_WIFI_STATE_TIMEOUT);
380     }
381 
382     @Override
waitForWifiEnabled(long timeout)383     public boolean waitForWifiEnabled(long timeout) throws DeviceNotAvailableException {
384         long startTime = System.currentTimeMillis();
385 
386         while (System.currentTimeMillis() < (startTime + timeout)) {
387             if (isWifiEnabled()) {
388                 return true;
389             }
390             getRunUtil().sleep(getPollTime());
391         }
392         return false;
393     }
394 
395     /**
396      * {@inheritDoc}
397      */
398     @Override
waitForWifiDisabled()399     public boolean waitForWifiDisabled() throws DeviceNotAvailableException {
400         return waitForWifiDisabled(DEFAULT_WIFI_STATE_TIMEOUT);
401     }
402 
403     @Override
waitForWifiDisabled(long timeout)404     public boolean waitForWifiDisabled(long timeout) throws DeviceNotAvailableException {
405         long startTime = System.currentTimeMillis();
406 
407         while (System.currentTimeMillis() < (startTime + timeout)) {
408             if (!isWifiEnabled()) {
409                 return true;
410             }
411             getRunUtil().sleep(getPollTime());
412         }
413         return false;
414     }
415 
416     /**
417      * {@inheritDoc}
418      */
419     @Override
getWifiInfo()420     public Map<String, String> getWifiInfo() throws DeviceNotAvailableException {
421         Map<String, String> info = new HashMap<>();
422 
423         final String result = runWifiUtil("getWifiInfo");
424         if (result != null) {
425             try {
426                 final JSONObject json = new JSONObject(result);
427                 final Iterator<?> keys = json.keys();
428                 while (keys.hasNext()) {
429                     final String key = (String)keys.next();
430                     info.put(key, json.getString(key));
431                 }
432             } catch(final JSONException e) {
433                 CLog.w("Failed to parse wifi info: %s", e.getMessage());
434             }
435         }
436 
437         return info;
438     }
439 
440     /**
441      * {@inheritDoc}
442      */
443     @Override
checkConnectivity(String urlToCheck)444     public boolean checkConnectivity(String urlToCheck) throws DeviceNotAvailableException {
445         return asBool(runWifiUtil("checkConnectivity", "urlToCheck", urlToCheck));
446     }
447 
448     /**
449      * {@inheritDoc}
450      */
451     @Override
connectToNetwork(String ssid, String psk, String urlToCheck)452     public boolean connectToNetwork(String ssid, String psk, String urlToCheck)
453             throws DeviceNotAvailableException {
454         return connectToNetwork(ssid, psk, urlToCheck, false);
455     }
456 
457     /**
458      * {@inheritDoc}
459      */
460     @Override
connectToNetwork(String ssid, String psk, String urlToCheck, boolean scanSsid)461     public boolean connectToNetwork(String ssid, String psk, String urlToCheck,
462             boolean scanSsid) throws DeviceNotAvailableException {
463         if (!enableWifi()) {
464             CLog.e("Failed to enable wifi");
465             return false;
466         }
467         if (!asBool(runWifiUtil("connectToNetwork", "ssid", ssid, "psk", psk, "urlToCheck",
468                 urlToCheck, "scan_ssid", Boolean.toString(scanSsid)))) {
469             return false;
470         }
471         return true;
472     }
473 
474     /**
475      * {@inheritDoc}
476      */
477     @Override
disconnectFromNetwork()478     public boolean disconnectFromNetwork() throws DeviceNotAvailableException {
479         if (!asBool(runWifiUtil("disconnectFromNetwork"))) {
480             return false;
481         }
482         if (!disableWifi()) {
483             CLog.e("Failed to disable wifi");
484             return false;
485         }
486         return true;
487     }
488 
489     /**
490      * {@inheritDoc}
491      */
492     @Override
startMonitor(long interval, String urlToCheck)493     public boolean startMonitor(long interval, String urlToCheck) throws DeviceNotAvailableException {
494         return asBool(runWifiUtil("startMonitor", "interval", Long.toString(interval), "urlToCheck",
495                 urlToCheck));
496     }
497 
498     /**
499      * {@inheritDoc}
500      */
501     @Override
stopMonitor()502     public List<Long> stopMonitor() throws DeviceNotAvailableException {
503         final String output = runWifiUtil("stopMonitor");
504         if (output == null || output.isEmpty() || NULL.equals(output)) {
505             return new ArrayList<Long>(0);
506         }
507 
508         String[] tokens = output.split(",");
509         List<Long> values = new ArrayList<Long>(tokens.length);
510         for (final String token : tokens) {
511             values.add(Long.parseLong(token));
512         }
513         return values;
514     }
515 
516     /**
517      * Run a WifiUtil command and return the result
518      *
519      * @param method the WifiUtil method to call
520      * @param args a flat list of [arg-name, value] pairs to pass
521      * @return The value of the result field in the output, or <code>null</code> if result could
522      * not be parsed
523      */
runWifiUtil(String method, String... args)524     private String runWifiUtil(String method, String... args) throws DeviceNotAvailableException {
525         final String cmd = buildWifiUtilCmd(method, args);
526 
527         WifiUtilOutput parser = new WifiUtilOutput();
528         mDevice.executeShellCommand(cmd, parser, WIFIUTIL_CMD_TIMEOUT_MINUTES, TimeUnit.MINUTES, 0);
529         if (parser.getError() != null) {
530             String errorMessage =
531                     String.format(
532                             "Failed to %s due to: '%s'. See logcat for details.",
533                             method, parser.getError());
534             CLog.e(errorMessage);
535         }
536         return parser.getResult();
537     }
538 
539     /**
540      * Build and return a WifiUtil command for the specified method and args
541      *
542      * @param method the WifiUtil method to call
543      * @param args a flat list of [arg-name, value] pairs to pass
544      * @return the command to be executed on the device shell
545      */
buildWifiUtilCmd(String method, String... args)546     static String buildWifiUtilCmd(String method, String... args) {
547         Map<String, String> argMap = new HashMap<String, String>();
548         argMap.put("method", method);
549         if ((args.length & 0x1) == 0x1) {
550             throw new IllegalArgumentException(
551                     "args should have even length, consisting of key and value pairs");
552         }
553         for (int i = 0; i < args.length; i += 2) {
554             // Skip null parameters
555             if (args[i+1] == null) {
556                 continue;
557             }
558             argMap.put(args[i], args[i+1]);
559         }
560         return buildWifiUtilCmdFromMap(argMap);
561     }
562 
563     /**
564      * Build and return a WifiUtil command for the specified args
565      *
566      * @param args A Map of (arg-name, value) pairs to pass as "-e" arguments to the `am` command
567      * @return the commadn to be executed on the device shell
568      */
buildWifiUtilCmdFromMap(Map<String, String> args)569     static String buildWifiUtilCmdFromMap(Map<String, String> args) {
570         StringBuilder sb = new StringBuilder("am instrument");
571 
572         for (Map.Entry<String, String> arg : args.entrySet()) {
573             sb.append(" -e ");
574             sb.append(arg.getKey());
575             sb.append(" ");
576             sb.append(quote(arg.getValue()));
577         }
578 
579         sb.append(" -w ");
580         sb.append(INSTRUMENTATION_PKG);
581         sb.append("/");
582         sb.append(INSTRUMENTATION_CLASS);
583 
584         return sb.toString();
585     }
586 
587     /**
588      * Helper function to convert a String to an Integer
589      */
asInt(String str)590     private static int asInt(String str) {
591         if (str == null) {
592             return -1;
593         }
594         try {
595             return Integer.parseInt(str);
596         } catch (NumberFormatException e) {
597             return -1;
598         }
599     }
600 
601     /**
602      * Helper function to convert a String to a boolean.  Maps "true" to true, and everything else
603      * to false.
604      */
asBool(String str)605     private static boolean asBool(String str) {
606         return "true".equals(str);
607     }
608 
609     /**
610      * Helper function to wrap the specified String in double-quotes to prevent shell interpretation
611      */
quote(String str)612     private static String quote(String str) {
613         return String.format("\"%s\"", str);
614     }
615 
616     /**
617      * Processes the output of a WifiUtil invocation
618      */
619     private static class WifiUtilOutput extends MultiLineReceiver {
620         private static final Pattern RESULT_PAT =
621                 Pattern.compile("INSTRUMENTATION_RESULT: result=(.*)");
622         private static final Pattern ERROR_PAT =
623                 Pattern.compile("INSTRUMENTATION_RESULT: error=(.*)");
624 
625         private String mResult = null;
626         private String mError = null;
627 
628         /**
629          * {@inheritDoc}
630          */
631         @Override
processNewLines(String[] lines)632         public void processNewLines(String[] lines) {
633             for (String line : lines) {
634                 Matcher resultMatcher = RESULT_PAT.matcher(line);
635                 if (resultMatcher.matches()) {
636                     mResult = resultMatcher.group(1);
637                     continue;
638                 }
639 
640                 Matcher errorMatcher = ERROR_PAT.matcher(line);
641                 if (errorMatcher.matches()) {
642                     mError = errorMatcher.group(1);
643                 }
644             }
645         }
646 
647         /**
648          * Return the result flag parsed from instrumentation output. <code>null</code> is returned
649          * if result output was not present.
650          */
getResult()651         String getResult() {
652             return mResult;
653         }
654 
getError()655         String getError() {
656             return mError;
657         }
658 
659         /**
660          * {@inheritDoc}
661          */
662         @Override
isCancelled()663         public boolean isCancelled() {
664             return false;
665         }
666     }
667 
668     /** {@inheritDoc} */
669     @Override
cleanUp()670     public void cleanUp() throws DeviceNotAvailableException {
671         String output = mDevice.uninstallPackage(INSTRUMENTATION_PKG);
672         if (output != null) {
673             CLog.w("Error '%s' occurred when uninstalling %s", output, INSTRUMENTATION_PKG);
674         } else {
675             CLog.d("Successfully clean up WifiHelper.");
676         }
677     }
678 }
679 
680