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