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