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