Tuesday, 23 August 2016

Programatically connecting to a WiFi network with Captive Portal on Marshmallow

Android platform is continuously growing and coming up with new concepts and features that are optimized to save battery or provide better user experience. Each new Android update comes up with new APIs along with changes in older ones. At times these changes might need developers to handle additional scenarios to provide features that are supported in newer versions.

One of such change was seen with an upgrade of Android OS to Lollipop and Marshmallow. With these upgrades the system changed how it handles connection to Networks. Starting from Lollipop, if a device is connected to both 3G and WiFi where WiFi network doesn't have internet connectivity, the device uses 3G network to send data. This was done so that devices don't automatically start using WiFi network that has restrictions or has a captive portal to connect to internet.

With start in active development in IoT field, there might be use cases where a device might want to connect to a local WiFi network that doesn't provide internet. There might be other use cases where your app might want to connect to a WiFi network that is behind captive portal and for automatically logging in to the network you might need to route your internet traffic through WiFi to make API calls needed to authenticate.

For such scenarios, Android does provide mechanisms for your app to explicitly use a particular network with specific capabilities to route all or parts of it's network traffic.

Here is a scenario where an app wants to connect to a WiFi network (which is behind a captive portal) using it's SSID and switch to start using WiFi network.


*App on Marshmallow needs ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission and Location services should be enabled to get wifi scan results

Steps to follow (Assuming that WiFi on device is enabled):
  1. Scan for new networks should be initiated to get latest available networks. But in this case we would assume that a particular network is already available, found during scan initiated by the system automatically. Create a network request and register for callback in which app should bind to network when available. This would ensure that all the network traffic for current app is routed through this network.
@TargetApi(LOLLIPOP) public void bindToNetwork(final String networkSSID, final NetworkStateChangeListener listener) { if (SDK_INT < LOLLIPOP) { logger.i("SDK version is below Lollipop. No need to bind process to network."); return; } NetworkRequest request = new NetworkRequest.Builder() .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build(); networkCallback = networkCallback(networkSSID, listener); manager.registerNetworkCallback(request, networkCallback); } @TargetApi(LOLLIPOP) private NetworkCallback networkCallback(final String networkSSID, final NetworkStateChangeListener listener) { return new NetworkCallback() { @Override public void onAvailable(Network network) { NetworkInfo networkInfo = manager.getNetworkInfo(network); logger.i("Network is Available. Network Info: " + networkInfo); if (WifiHelper.areEqual(networkInfo.getExtraInfo(), networkSSID)) { manager.unregisterNetworkCallback(this); networkCallback = null; bindToRequiredNetwork(network); logger.i(format("Bound application to use %s network", networkSSID)); listener.onNetworkBound(); //do required stuff in this method } } }; } @TargetApi(LOLLIPOP) public void bindToRequiredNetwork(Network network) { if (SDK_INT >= M) { manager.bindProcessToNetwork(network); } else { ConnectivityManager.setProcessDefaultNetwork(network); } }
Now the callback would be received when device is connected to required network.
  1. Create a open WifiConfiguration with required SSID to which your app needs to connect. 
  2. Add the network to the list of configured networks by calling WifiManager.addNetwork(configuration)
A restriction that was put starting from Marshmallow was that the app can only modify those WiFi networks that it has created. The system doesn't allow modification of networks that other apps or users have created. So, adding the network might fail if the network already exists in the list (the system might auto connect to network once it's detected). Additional steps are needed to handle such a scenario:
  1. Check networkId returned by addNetwork(), in case its -1 (unable to add network with configuration), try finding network with same SSID among list of existing networks. 
  2. In case any network with same SSID already exists, get networkId of that network.
  3. Ultimately, only call enableNetwork(networkId, true) if a valid networkId exists. I would never call enableNetwork(-1, true) on Marshmallow.

So, lets implement this:


public boolean connectToSSID(String SSID) { WifiConfiguration configuration = createOpenWifiConfiguration(SSID); int networkId = wifiManager.addNetwork(configuration); logger.d("networkId assigned while adding network is " + networkId); return enableNetwork(SSID, networkId); } private WifiConfiguration createOpenWifiConfiguration(String SSID) { WifiConfiguration configuration = new WifiConfiguration(); configuration.SSID = formatSSID(SSID); configuration.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE); assignHighestPriority(configuration); return configuration; } private boolean enableNetwork(String SSID, int networkId) { if (networkId == -1) { networkId = getExistingNetworkId(SSID); if (networkId == -1) { logger.e("Couldn't add network with SSID: " + SSID); return false; } } return wifiManager.enableNetwork(networkId, true); } private int getExistingNetworkId(String SSID) { List<WifiConfiguration> configuredNetworks = wifiManager.getConfiguredNetworks(); if (configuredNetworks != null) { for (WifiConfiguration existingConfig : configuredNetworks) { if (areEqual(trimQuotes(existingConfig.SSID), trimQuotes(SSID))) { return existingConfig.networkId; } } } return -1; } //To tell OS to give preference to this network private void assignHighestPriority(WifiConfiguration config) { List<WifiConfiguration> configuredNetworks = wifiManager.getConfiguredNetworks(); if (configuredNetworks != null) { for (WifiConfiguration existingConfig : configuredNetworks) { if (config.priority <= existingConfig.priority) { config.priority = existingConfig.priority + 1; } } } } private static String formatSSID(String wifiSSID) { return format("\"%s\"", wifiSSID); } private static String trimQuotes(String str) { if (!isEmpty(str)) { return str.replaceAll("^\"*", "").replaceAll("\"*$", ""); } return str; }

Calling connectToSSID() will connect the device to network with given SSID. This will in turn provide a callback to onAvailable() of NetworkCallback in which the app would bind to this network. After binding is done, listener.onNetworkBound() is called in which a login request could be initiated.

Once login is done, to tell the device that internet connectivity is available now in this network so that it becomes an active network and all android network traffic is routed through this network, you would need to call

@TargetApi(LOLLIPOP) public void reportBoundNetworkConnectivity() { if (SDK_INT < LOLLIPOP) { return; } if (SDK_INT >= M) { Network defaultNetwork = manager.getBoundNetworkForProcess(); manager.reportNetworkConnectivity(defaultNetwork, true); } else { Network defaultNetwork = ConnectivityManager.getProcessDefaultNetwork(); manager.reportBadNetwork(defaultNetwork); } }

This will make the OS to check if there is internet connectivity available in the network to which app is bound. If there is, OS would switch to make it an active network. Lastly, app should unbind from the network.

If you are looking to solve similar problems or need to connect to WiFi networks in your project for similar reasons, there is an open source android library that provides all these functionalities and is easy to use. I would recommend you to give it a shot: android-wificonnect

15 comments:

  1. Thanks for the tut, I had to modify it a little for my specific needs but it connects to a wifi enabled device now.

    ReplyDelete
  2. Hey!
    I am working on an app that will you this functionality.
    However, In the above explanation i could not understand what will be the components of NetworkStateChangeListner?

    ReplyDelete
    Replies
    1. It's just a custom listener that can be notified when onAvailable NetworkCallback is received. You can have a look at complete code here: https://github.com/nishkarsh/android-wificonnect

      Delete
  3. Hi Nishkarsh,

    I have been trying to connect to my wifi router programatically for some time now. I have more details at http://stackoverflow.com/questions/41712432/connecting-to-wifi-network-automatically-in-android-5-and-6 Are you familiar with the issue? Do you have any pointers?

    Thanks
    Arjun

    ReplyDelete
    Replies
    1. Hey Arjun,

      Can you please let me know if mobile data is turned on at the time the app is trying to connect to WiFi? I have not yet got the time to try it out. Can you try it out with different states of WiFi (when it's already disabled or connected to some other SSID) and without having a sim card (or mobile data turned off). This might not even be related but might help in finding out the exact problem. I will myself try it out and let you know in case I find something. :)

      Sorry for the delayed reply!

      Delete

    2. I have the following observations.

      1. The issue so far is specific to 1 android 6 Samsung tablet and 1 android 5 Micromax phone. It works just fine on 3 other android 6 phones, 1 android N phone and Android 4.4.2 phone.
      2. The access point ends up in wifi_ap_disabled state in the problematic cases consistently. Both addNetwork and enableNetwork calls are affirmative.
      3. These access points are not that of a router wifi but that of other phones that broadcast. The problematic phones can programatically connect to wifi hotspots (setup manually and not in the programatic way as I would like to) without any issue.
      4. SIM enabled/disabled state or wifi state with a different connected network doesn't change the dynamics for both working and problematic phones.

      This makes me think that it is a combination of phones/tabs (and not OS) and the access point broadcast configuration. Do you think I should be playing around with some config parameters?

      Thanks
      Arjun

      Delete
    3. Same here. Android N works fine here, too. It's a Samsung Galaxy Tab A T585 which makes trouble. On a rooted device it can be fixed with "su" and "settings put global captive_portal_detection_enabled 0", now delete your stored wifi configurations (aka networks) and add them again (do this in the android settings).

      Delete
  4. Hi,
    I've implement this code but i can't connect besides the code returns me an idnetwork.
    http://stackoverflow.com/questions/42890588/how-do-i-connect-to-a-ssid-selected-by-the-user

    Can you please help me?

    ReplyDelete
    Replies
    1. Hey Bernardo!
      Are you getting a non-negative networkId? If that's the case, can you try listening to NETWORK_STATE_CHANGED_ACTION broadcast and tell your observations.

      Delete
  5. Hello Nishkarsh,

    I have same requirement in my app which you have explained. I used your code to and it is working fine on all os.

    But I am, facing one issue. Case is like below.
    1. Wifi is enabled.
    2. Location is disabled.
    3. Wifi is latched to my captive portal(Android OS latched it to wifi).
    4. I am checking whether wifi is latched to my wifi network using below code

    WifiInfo wifiInfo = wifiManager.getConnectionInfo();
    if (wifiInfo != null && WifiUtils.IsMyWifi(wifiInfo.getSSID(), context) && wifiInfo.getIpAddress() != 0 && wifiInfo.getNetworkId() != -1)
    return true;
    return false;

    5. If it latched to my network then I am hitting capetive portal but it is giving me 200 always. As per my observation app network is not bind to wifi.

    How I can bind the app network to wifi, when wifi is already latched by os.

    Can you please help me to resolve this issue?

    ReplyDelete
    Replies
    1. Hey Abhijeet!

      Could you please tell which OS version are you trying this on? Also if I understand it correctly, you are facing problems binding to a WiFi network which is behind captive portal and when you already have mobile data turned on. Am I right on this? In that case, could you please tell if it all works when mobile data is turned off or when location is enabled?

      Delete
  6. HI Nishkarsh..Thanks you for this porst.Is your program only for open networks?? i read about this in your github README. Do you have any idea how we can authenticate for WPA/WEP.

    ReplyDelete
    Replies
    1. Hey Rishi,

      Sorry for delayed reply! As of now, the library supports only initiating connection to open networks, however, it shouldn't take much efforts to modify it to connect to networks that require authentication. I have planned it to have this support for future release but haven't been getting much time lately.

      If you haven't done it already, lemme know and I can help you through the steps to do it.

      Delete
  7. When you initially set up the hotspot feature on your iPhone, you are reminded that you can also use bluetooth to share the connection.http://www.chrismartslaw.com/

    ReplyDelete