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