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