1 /* 2 * Copyright (C) 2014 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 17 package com.android.tradefed.utils.wifi; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.net.wifi.SupplicantState; 22 import android.net.wifi.WifiConfiguration; 23 import android.net.wifi.WifiInfo; 24 import android.net.wifi.WifiManager; 25 import android.os.SystemClock; 26 import android.util.Log; 27 28 import org.json.JSONException; 29 import org.json.JSONObject; 30 31 import java.io.IOException; 32 import java.net.HttpURLConnection; 33 import java.net.MalformedURLException; 34 import java.net.URL; 35 import java.util.BitSet; 36 import java.util.List; 37 import java.util.concurrent.Callable; 38 39 /** 40 * A helper class to connect to wifi networks. 41 */ 42 public class WifiConnector { 43 44 private static final String TAG = WifiConnector.class.getSimpleName(); 45 private static final long DEFAULT_TIMEOUT = 120 * 1000; 46 private static final long DEFAULT_WAIT_TIME = 5 * 1000; 47 private static final long POLL_TIME = 1000; 48 49 private Context mContext; 50 private WifiManager mWifiManager; 51 52 /** 53 * Thrown when an error occurs while manipulating Wi-Fi services. 54 */ 55 public static class WifiException extends Exception { 56 WifiException(String msg)57 public WifiException(String msg) { 58 super(msg); 59 } 60 WifiException(String msg, Throwable cause)61 public WifiException(String msg, Throwable cause) { 62 super(msg, cause); 63 } 64 65 } 66 WifiConnector(final Context context)67 public WifiConnector(final Context context) { 68 mContext = context; 69 mWifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE); 70 } 71 quote(String str)72 private static String quote(String str) { 73 return String.format("\"%s\"", str); 74 } 75 76 /** 77 * Waits until an expected condition is satisfied for {@code timeout}. 78 * 79 * @param checker a <code>Callable</code> to check the expected condition 80 * @param description a description of what this callable is doing 81 * @param timeout the duration to wait (millis) for the expected condition 82 * @throws WifiException if DEFAULT_TIMEOUT expires 83 * @return time in millis spent waiting 84 */ waitForCallable(final Callable<Boolean> checker, final String description, final long timeout)85 private long waitForCallable(final Callable<Boolean> checker, final String description, 86 final long timeout) 87 throws WifiException { 88 if (timeout <= 0) { 89 throw new WifiException( 90 String.format("Failed %s due to invalid timeout (%d ms)", description, timeout)); 91 } 92 long startTime = SystemClock.uptimeMillis(); 93 long endTime = startTime + timeout; 94 try { 95 while (SystemClock.uptimeMillis() < endTime) { 96 if (checker.call()) { 97 long elapsed = SystemClock.uptimeMillis() - startTime; 98 Log.i(TAG, String.format( 99 "Time elapsed waiting for %s: %d ms", description, elapsed)); 100 return elapsed; 101 } 102 Thread.sleep(POLL_TIME); 103 } 104 } catch (final Exception e) { 105 throw new WifiException("failed to wait for callable", e); 106 } 107 throw new WifiException( 108 String.format("Failed %s due to exceeding timeout (%d ms)", description, timeout)); 109 } 110 waitForCallable(final Callable<Boolean> checker, final String description)111 private void waitForCallable(final Callable<Boolean> checker, final String description) 112 throws WifiException { 113 waitForCallable(checker, description, DEFAULT_TIMEOUT); 114 } 115 116 /** 117 * Adds a Wi-Fi network configuration. 118 * 119 * @param ssid SSID of a Wi-Fi network 120 * @param psk PSK(Pre-Shared Key) of a Wi-Fi network. This can be null if the given SSID is for 121 * an open network. 122 * @return the network ID of a new network configuration 123 * @throws WifiException if the operation fails 124 */ addNetwork(final String ssid, final String psk, final boolean scanSsid)125 public int addNetwork(final String ssid, final String psk, final boolean scanSsid) 126 throws WifiException { 127 // Skip adding network if it's already added in the device 128 // TODO: Fix the permission issue for the APK to add/update already added network 129 int networkId = getNetworkId(ssid); 130 if (networkId >= 0) { 131 return networkId; 132 } 133 final WifiConfiguration config = new WifiConfiguration(); 134 // A string SSID _must_ be enclosed in double-quotation marks 135 config.SSID = quote(ssid); 136 137 if (scanSsid) { 138 config.hiddenSSID = true; 139 } 140 141 if (psk == null) { 142 // KeyMgmt should be NONE only 143 final BitSet keymgmt = new BitSet(); 144 keymgmt.set(WifiConfiguration.KeyMgmt.NONE); 145 config.allowedKeyManagement = keymgmt; 146 } else { 147 config.preSharedKey = quote(psk); 148 } 149 networkId = mWifiManager.addNetwork(config); 150 if (-1 == networkId) { 151 throw new WifiException("failed to add network"); 152 } 153 154 return networkId; 155 } 156 getNetworkId(String ssid)157 private int getNetworkId(String ssid) { 158 List<WifiConfiguration> netlist = mWifiManager.getConfiguredNetworks(); 159 for (WifiConfiguration config : netlist) { 160 if (quote(ssid).equals(config.SSID)) { 161 return config.networkId; 162 } 163 } 164 return -1; 165 } 166 167 /** 168 * Removes all Wi-Fi network configurations. 169 * 170 * @param throwIfFail <code>true</code> if a caller wants an exception to be thrown when the 171 * operation fails. Otherwise <code>false</code>. 172 * @throws WifiException if the operation fails 173 */ removeAllNetworks(boolean throwIfFail)174 public void removeAllNetworks(boolean throwIfFail) throws WifiException { 175 List<WifiConfiguration> netlist = mWifiManager.getConfiguredNetworks(); 176 if (netlist != null) { 177 int failCount = 0; 178 for (WifiConfiguration config : netlist) { 179 if (!mWifiManager.removeNetwork(config.networkId)) { 180 Log.w(TAG, String.format("failed to remove network id %d (SSID = %s)", 181 config.networkId, config.SSID)); 182 failCount++; 183 } 184 } 185 if (0 < failCount && throwIfFail) { 186 throw new WifiException("failed to remove all networks."); 187 } 188 } 189 } 190 191 /** 192 * Check network connectivity by sending a HTTP request to a given URL. 193 * 194 * @param urlToCheck URL to send a test request to 195 * @return <code>true</code> if the test request succeeds. Otherwise <code>false</code>. 196 */ checkConnectivity(final String urlToCheck)197 public static boolean checkConnectivity(final String urlToCheck) { 198 URL url = null; 199 try { 200 url = new URL(urlToCheck); 201 } catch (MalformedURLException e) { 202 throw new RuntimeException("Malformed URL", e); 203 } 204 HttpURLConnection urlConnection = null; 205 try { 206 urlConnection = (HttpURLConnection) url.openConnection(); 207 urlConnection.connect(); 208 } catch (final IOException e) { 209 return false; 210 } finally { 211 if (urlConnection != null) { 212 urlConnection.disconnect(); 213 } 214 } 215 return true; 216 } 217 218 /** 219 * Connects a device to a given Wi-Fi network and check connectivity. 220 * 221 * @param ssid SSID of a Wi-Fi network 222 * @param psk PSK of a Wi-Fi network 223 * @param urlToCheck URL to use when checking connectivity 224 * @param connectTimeout duration in seconds to wait for connecting to the network or 225 {@code DEFAULT_TIMEOUT} millis if -1 is passed. 226 * @param scanSsid whether to scan for hidden SSID for this network 227 * @throws WifiException if the operation fails 228 */ connectToNetwork(final String ssid, final String psk, final String urlToCheck, long connectTimeout, final boolean scanSsid)229 public void connectToNetwork(final String ssid, final String psk, final String urlToCheck, 230 long connectTimeout, final boolean scanSsid) 231 throws WifiException { 232 if (!mWifiManager.isWifiEnabled()) { 233 throw new WifiException("wifi not enabled"); 234 } 235 236 updateLastNetwork(ssid, psk, scanSsid); 237 238 connectTimeout = connectTimeout == -1 ? DEFAULT_TIMEOUT : (connectTimeout * 1000); 239 long timeSpent; 240 timeSpent = waitForCallable(new Callable<Boolean>() { 241 @Override 242 public Boolean call() throws Exception { 243 return mWifiManager.isWifiEnabled(); 244 } 245 }, "enabling wifi", connectTimeout); 246 247 // Wait for some seconds to let wifi to be stable. This increases the chance of success for 248 // subsequent operations. 249 try { 250 Thread.sleep(DEFAULT_WAIT_TIME); 251 } catch (InterruptedException e) { 252 throw new WifiException(String.format("failed to sleep for %d ms", DEFAULT_WAIT_TIME), 253 e); 254 } 255 256 removeAllNetworks(false); 257 258 final int networkId = addNetwork(ssid, psk, scanSsid); 259 if (!mWifiManager.enableNetwork(networkId, true)) { 260 throw new WifiException(String.format("failed to enable network %s", ssid)); 261 } 262 if (!mWifiManager.saveConfiguration()) { 263 Log.w(TAG, String.format("failed to save configuration %s", ssid)); 264 } 265 connectTimeout = calculateTimeLeft(connectTimeout, timeSpent); 266 timeSpent = waitForCallable(new Callable<Boolean>() { 267 @Override 268 public Boolean call() throws Exception { 269 final SupplicantState state = mWifiManager.getConnectionInfo() 270 .getSupplicantState(); 271 return SupplicantState.COMPLETED == state; 272 } 273 }, String.format("associating to network (ssid: %s)", ssid), connectTimeout); 274 275 connectTimeout = calculateTimeLeft(connectTimeout, timeSpent); 276 timeSpent = waitForCallable(new Callable<Boolean>() { 277 @Override 278 public Boolean call() throws Exception { 279 final WifiInfo info = mWifiManager.getConnectionInfo(); 280 return 0 != info.getIpAddress(); 281 } 282 }, String.format("dhcp assignment (ssid: %s)", ssid), connectTimeout); 283 284 connectTimeout = calculateTimeLeft(connectTimeout, timeSpent); 285 waitForCallable(new Callable<Boolean>() { 286 @Override 287 public Boolean call() throws Exception { 288 return checkConnectivity(urlToCheck); 289 } 290 }, String.format("request to %s (ssid: %s)", urlToCheck, ssid), connectTimeout); 291 } 292 293 /** 294 * Connects a device to a given Wi-Fi network and check connectivity using 295 * 296 * @param ssid SSID of a Wi-Fi network 297 * @param psk PSK of a Wi-Fi network 298 * @param urlToCheck URL to use when checking connectivity 299 * @param connectTimeout duration in seconds to wait for connecting to the network or 300 {@code DEFAULT_TIMEOUT} millis if -1 is passed. 301 * @throws WifiException if the operation fails 302 */ connectToNetwork(final String ssid, final String psk, final String urlToCheck, long connectTimeout)303 public void connectToNetwork(final String ssid, final String psk, final String urlToCheck, 304 long connectTimeout) 305 throws WifiException { 306 connectToNetwork(ssid, psk, urlToCheck, -1, false); 307 } 308 309 /** 310 * Connects a device to a given Wi-Fi network and check connectivity using 311 * {@code DEFAULT_TIMEOUT}. 312 * 313 * @param ssid SSID of a Wi-Fi network 314 * @param psk PSK of a Wi-Fi network 315 * @param urlToCheck URL to use when checking connectivity 316 * @throws WifiException if the operation fails 317 */ connectToNetwork(final String ssid, final String psk, final String urlToCheck)318 public void connectToNetwork(final String ssid, final String psk, final String urlToCheck) 319 throws WifiException { 320 connectToNetwork(ssid, psk, urlToCheck, -1); 321 } 322 323 /** 324 * Disconnects a device from Wi-Fi network and disable Wi-Fi. 325 * 326 * @throws WifiException if the operation fails 327 */ disconnectFromNetwork()328 public void disconnectFromNetwork() throws WifiException { 329 if (mWifiManager.isWifiEnabled()) { 330 removeAllNetworks(false); 331 } 332 } 333 334 /** 335 * Returns Wi-Fi information of a device. 336 * 337 * @return a {@link JSONObject} containing the current Wi-Fi status 338 * @throws WifiException if the operation fails 339 */ getWifiInfo()340 public JSONObject getWifiInfo() throws WifiException { 341 final JSONObject json = new JSONObject(); 342 343 try { 344 final WifiInfo info = mWifiManager.getConnectionInfo(); 345 json.put("ssid", info.getSSID()); 346 json.put("bssid", info.getBSSID()); 347 json.put("hiddenSsid", info.getHiddenSSID()); 348 final int addr = info.getIpAddress(); 349 // IP address is stored with the first octet in the lowest byte 350 final int a = (addr >> 0) & 0xff; 351 final int b = (addr >> 8) & 0xff; 352 final int c = (addr >> 16) & 0xff; 353 final int d = (addr >> 24) & 0xff; 354 json.put("ipAddress", String.format("%s.%s.%s.%s", a, b, c, d)); 355 json.put("linkSpeed", info.getLinkSpeed()); 356 json.put("rssi", info.getRssi()); 357 json.put("macAddress", info.getMacAddress()); 358 } catch (final JSONException e) { 359 throw new WifiException(e.toString()); 360 } 361 362 return json; 363 } 364 365 /** 366 * Reconnects a device to a last connected Wi-Fi network and check connectivity. 367 * 368 * @param urlToCheck URL to use when checking connectivity 369 * @throws WifiException if the operation fails 370 */ reconnectToLastNetwork(String urlToCheck)371 public void reconnectToLastNetwork(String urlToCheck) throws WifiException { 372 final SharedPreferences prefs = mContext.getSharedPreferences(TAG, 0); 373 final String ssid = prefs.getString("ssid", null); 374 final String psk = prefs.getString("psk", null); 375 final boolean scanSsid = prefs.getBoolean("scan_ssid", false); 376 if (ssid == null) { 377 throw new WifiException("No last connected network."); 378 } 379 connectToNetwork(ssid, psk, urlToCheck, -1, scanSsid); 380 } 381 updateLastNetwork(final String ssid, final String psk, final boolean scanSsid)382 private void updateLastNetwork(final String ssid, final String psk, final boolean scanSsid) { 383 final SharedPreferences prefs = mContext.getSharedPreferences(TAG, 0); 384 final SharedPreferences.Editor editor = prefs.edit(); 385 editor.putString("ssid", ssid); 386 editor.putString("psk", psk); 387 editor.putBoolean("scan_ssid", scanSsid); 388 editor.commit(); 389 } 390 calculateTimeLeft(long connectTimeout, long timeSpent)391 private long calculateTimeLeft(long connectTimeout, long timeSpent) { 392 if (timeSpent > connectTimeout) { 393 return 0; 394 } else { 395 return connectTimeout - timeSpent; 396 } 397 } 398 } 399