Home | History | Annotate | Download | only in wifi
      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