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