Android : Reconnect to Wi-Fi after entering coverage area while screen turned off
Asked Answered
H

3

4

I have been struggling with automatically turning on Wi-Fi as soon as the device is within range of an access point without turning on the screen. It has been very frustrating to test and figure out a solution, especially since different devices have completely different results.

Base Test
Keep the screen turned off during this test. The application should hold a WifiLock.

  1. Walk out of WiFi coverage and stay there for a minute.
  2. Walk back into coverage.

Result : The Wifi is not reconnected immediately and thus app does not reconnect to server. Depending on the device and settings, sometimes it would not reconnect at all until the screen was turned on.

Forcing the Wi-Fi to reconnect
Ok, this time my application calls WifiManager.Reconnect() at an interval if Wifi is disconnected.

Repeated the test. Results : Worked for the S3, failed for other devices.

Tried adding some other calls
Tried different combinations of WifiManager.Scan(), WifiManager.Reassociate(), ...etc. Eventually it was working for most devices(HTC, S3) except the S4.

Code that seems to work for all devices

NetworkInfo wifiInfo = _androidConnectivityMgr.GetNetworkInfo(ConnectivityType.Wifi);
if (!_wifiManager.IsWifiEnabled || _wifiManager.WifiState == WifiState.Disabled || _wifiManager.WifiState == WifiState.Disabling)
{
    // Make sure the Wi-Fi is enabled, required for some devices when enable WiFi does not occur immediately
    _wifiManager.SetWifiEnabled(true);
}

if (!wifiInfo.IsConnectedOrConnecting)
{
    // Do not wait for the OS to initiate a reconnect to a Wi-Fi router
    _wifiManager.PingSupplicant();
    if (_wifiManager.WifiState == WifiState.Enabled)
    {
        try
        {
            // Brute force methods required for some devices
            _wifiManager.SetWifiEnabled(false);
            _wifiManager.SetWifiEnabled(true);
        }
        catch (Java.Lang.SecurityException)
        {
            // Catching exception which should not occur on most devices. OS bug details at :
            // https://code.google.com/p/android/issues/detail?id=22036
        }
    }
    _wifiManager.Disconnect();
    _wifiManager.StartScan();
    _wifiManager.Reassociate();
    _wifiManager.Reconnect();
}

I am not even sure all this code is necessary as I was unable to find much information online. WifiFixer did help some. But this does seem to work for the devices I have tested on.

The Question

  • Is there a better way of doing this?
  • Do the manufacturers really modify the base Android where I can be seeing this much of a difference?
  • Is this completely the wrong way to approach this?

Thanks for reading through all this :)

Additional Notes

  1. Code runs during 10+ second interval initiated from the AlarmManager. WakeLock is held only for the duration of this call.
  2. Before this final scary looking solution/hack the "Wifi Sleep Policy" affected the results. This confused me since I am holding a WifiLock the entire time, which I thought was equivalent of "Never".
  3. Changing the "Wifi Sleep Policy" programmatically does not work for the S4, can anyone else confirm?
  4. Yes, we have a specific need to do this and are aware of battery implication.
Helse answered 3/10, 2013 at 0:6 Comment(0)
H
0

Had a second go around in the area. While the above solution did work for all our qualified devices, there were too many calls that might have been unnecessary. Plus we got new device for which the solution did not work. Here is a much better solution:

At every interval this code is called

NetworkInfo wifiInfo = _androidConnectivityMgr.GetNetworkInfo(ConnectivityType.Wifi);
if (!wifiInfo.IsConnectedOrConnecting)
{
    // Need to make sure the CPU does not go to sleep before the following async calls are finished
    _wifiScanWakeLock.Acquire();

    // Do not wait for the OS to initiate a reconnect to a Wi-Fi router
    _wifiManager.StartScan();
}
  • _wifiScanWakeLock is just a partial, non-reference counted WakeLock, Dispose in OnDestroy

When the Wi-Fi scan is finished

private void OnWifiScanResultsReceived(Intent result)
{
    NetworkInfo wifiInfo = _androidConnectivityMgr.GetNetworkInfo(ConnectivityType.Wifi);
    if (!wifiInfo.IsConnectedOrConnecting)
    {
        Dictionary<string, int> savedNetworks = new Dictionary<string, int>();
        foreach (WifiConfiguration config in _wifiManager.ConfiguredNetworks)
        {
            string escapedSsid = Regex.Replace(config.Ssid, "^\"|\"$", String.Empty);
            savedNetworks[escapedSsid] = config.NetworkId;
        }

        foreach (ScanResult ap in _wifiManager.ScanResults)
        {
            int networkId;
            if (savedNetworks.TryGetValue(ap.Ssid, out networkId))
            {
                savedNetworks.Remove(ap.Ssid);
                _wifiManager.EnableNetwork(networkId, false);
            }
        }
    }
    _wifiScanWakeLock.Release();
}
  • BSSID for the WifiConfiguration is always null and cannot be used that to uniquely compare to the BSSID of the ScanResult
  • This is the core code, obviously you would have to worry about case of two identical SSIDs and other optimizations
Helse answered 18/3, 2014 at 15:52 Comment(2)
This is a really important question for my company, thanks for all your hard work and good documentation on this. Do you remember what event was triggering the above code? It's one thing to initiate a scan when the OS is kind enough to run you, but I'm finding partial wake lock + wifi lock + battery optimization exemption = still not enough to keep running when phone goes into full Doze (IDLE in ADB's BatteryStats). I've got a bounty out for this issue, if you look up my questions--I'd be happy to pay it out!Judy
Code above is triggered by the registering for the SCAN_RESULTS_AVAILABLE_ACTION intent, its in C# which has different format. Thats an interesting case, we also disabled doze mode for our application. I think one of the reasons we never ran into this case is because on connection loss we turn on the screen and notify the user which resets the doze mode counter. In our case users are not expected to be completely out of coverage for more than an hour. Would be interested to test this during a less busy period. As a bad solution, you could brute force turn on screen when doze mode is activated.Helse
V
2

My scenario is slightly different - I do not hold a wifi lock to begin with (and I am on regular android so I had to translate your method).

Screen off, CPU off, radio dies. Alarm wakes my (wakeful) service up - I hold a (partial) wake lock.

What I want is - if wifi is enabled to connect to the access point it was connected before the radio died - I acquire a wifi lock and I call your function - wakeWifiUp(). When the radio has died (!wifiInfo.IsConnectedOrConnecting is true) I get a network unreachable when I try to connect. I workaround it as in :

public final class NetworkService extends WakefulIntentService {

    // this is an intent service - runs on its own thread - otherwise it would
    // deadlock as I am using it. Moreover it holds a wakelock and woken up by
    // an AlarmManager's Receiver - works reliably
    private BroadcastReceiver mConnectionReceiver;
    private volatile static CountDownLatch latch;

    @Override
    protected void doWakefulWork(Intent intent) {
        WifiLock _wifiLock = null;
        WifiManager wm = (WifiManager) getSystemService(Context.WIFI_SERVICE);
        boolean failedToConnect = true;
        if (wm != null && wm.isWifiEnabled()) {// Don't want to enable it myself
            _wifiLock = wm.createWifiLock(
            /* WifiManager.WIFI_MODE_FULL_HIGH_PERF */0x3, this.getClass()
                .getName() + ".WIFI_LOCK");
            _wifiLock.acquire();
            failedToConnect = !wakeWifiUp();
        }
        if (failedToConnect) {
            if (_wifiLock != null) _wifiLock.release();
            w("No connection !");
            return;
        }
        HttpURLConnection connection = null;
        try {
            connection = connection(); 
        } catch (IOException e) {/* won't throw - it doesn't do much*/}
        OutputStream serverOutputStream = null;
        try {
            serverOutputStream = connection.getOutputStream(); // now
            // this is really where the connection might seriously throw
            // .... Work ....
        } catch (IOException e) {
            w("IOException sending data " + e.getMessage());
            // I get here : Network unreachable when radio dies
        } finally {
            if (_wifiLock != null) _wifiLock.release();
            if (connection != null) connection.disconnect();
        }
    }

    private HttpURLConnection connection() throws MalformedURLException,
            IOException {
        HttpURLConnection connection = (HttpURLConnection) new URL("localhost")
            .openConnection();
        connection.setDoOutput(true); // triggers POST
        connection.setRequestProperty("Connection", "Keep-Alive");
        connection.setRequestProperty("User-Agent",
            "Android Multipart HTTP Client 1.1");
        return connection;
    }

    private boolean wakeWifiUp() {
        ConnectivityManager _androidConnectivityMgr = (ConnectivityManager)
                getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo wifiInfo = _androidConnectivityMgr
            .getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        WifiManager _wifiManager = (WifiManager)
                getSystemService(Context.WIFI_SERVICE);
        final int wifiState = _wifiManager.getWifiState();
        if (!_wifiManager.isWifiEnabled()
            || wifiState == WifiManager.WIFI_STATE_DISABLED
            || wifiState == WifiManager.WIFI_STATE_DISABLING) {
            // Make sure the Wi-Fi is enabled, required for some devices when
            // enable WiFi does not occur immediately
            d("!_wifiManager.isWifiEnabled()");
            _wifiManager.setWifiEnabled(true);
            // do not enable if not enabled ! FIXME
            return false;
        }
        if (!wifiInfo.isConnectedOrConnecting()) {
            d("Wifi is NOT Connected Or Connecting - "
                + "wake it up and wait till is up");
            // Do not wait for the OS to initiate a reconnect to a Wi-Fi router
            _wifiManager.pingSupplicant();
            if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
                try {
                    // Brute force methods required for some devices
                    _wifiManager.setWifiEnabled(false);
                    _wifiManager.setWifiEnabled(true);
                } catch (SecurityException e) {
                    // Catching exception which should not occur on most
                    // devices. OS bug details at :
                    // https://code.google.com/p/android/issues/detail?id=22036
                }
            }
            _wifiManager.disconnect();
            _wifiManager.startScan();
            _wifiManager.reassociate();
            _wifiManager.reconnect();
            // THIS IS WHAT I DO TO WAIT FOR A CONNECTION
            try {
                mConnectionReceiver = new WifiConnectionMonitor();
                startMonitoringConnection();
                latch = new CountDownLatch(1);
                w("I wait");
                latch.await();
                w("Woke up");
                return true; // made it
            } catch (InterruptedException e) {
                w("Interrupted while waiting for connection", e);
                return false;
            } finally {
                stopMonitoringConnection();
            }
        }
        return true;
    }

    static void downTheLatch() {
        latch.countDown();
    }

    private synchronized void startMonitoringConnection() {
        IntentFilter aFilter = new IntentFilter(
            ConnectivityManager.CONNECTIVITY_ACTION);
        aFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        registerReceiver(mConnectionReceiver, aFilter);
    }

    private synchronized void stopMonitoringConnection() {
        unregisterReceiver(mConnectionReceiver);
    }

    private final class WifiConnectionMonitor extends BroadcastReceiver {

        @Override
        public void onReceive(Context context, Intent in) {
            String action = in.getAction();
            if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
                NetworkInfo networkInfo = in
                    .getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
                d(networkInfo + "");
                if (networkInfo.isConnected()) {
                    d("Wifi is connected!");
                    NetworkService.downTheLatch(); // HERE THE SERVICE IS WOKEN!
                }
            }
        }
    }
}

Btw not all the tricks in wakeWifiUp() are needed (in my case) and all the !_wifiManager.isWifiEnabled() may be ommited - as I only use the net if enabled by the user. I leave it for completeness.

Recap : in my scenario your method was not enough (if I translated correctly to java and didn't make some silly mistake, which always applies - see also my connection()). I needed to wait for connection to be established - but then all was fine. Not sure still how exactly you were using it - if as I do then the difference might be that you were holding a wifi lock all along

HTC Nexus 1, 2.3.7, Cyanogen mod (don't shoot I've been given it to test).

Will keep you posted

Verbena answered 14/11, 2013 at 3:1 Comment(1)
Not sure if the latch must be declared volatile btwVerbena
D
1

The approaches by Alex and Mr_and_Mrs_D were close but not entirely consistent under Android 4.4 KitKat (Nexus 4). This may have to do with Google's more aggressive power saving policies starting in KitKat. I used a combination of their approaches with modifications.

The general idea is that during a periodic WiFi check, explicitly start a scan, then in the scan results handler call reassociate() and reconnect(). Additionally in the NETWORK_STATE_CHANGED_ACTION callback, check whether the connection is established before releasing the wake lock. The key is to hold onto the wake lock long enough for the WiFi connection to properly establish (and obviously not any longer than necessary).

Periodic WiFi check that kicks things off:

public static void CheckWiFi() {  
    mWakeLock.acquire();
    if (!WiFi_Mgr.isWifiEnabled()) {
        WiFi_Mgr.setWifiEnabled(true);
    }
    WiFi_Mgr.startScan();

    //Set an alarm to fire after N seconds to release wake lock & shut off WiFi if no connection is available.
    // ... 
}

Register for WiFi broadcasts

//Setup WiFi connection status receiver
IntentFilter WiFiFilters = new IntentFilter();
WiFiFilters.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
WiFiFilters.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
this.registerReceiver(WiFiReceiver, WiFiFilters);

And the WiFi event handlers

private final BroadcastReceiver WiFiReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();

        if(WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
            NetworkInfo netInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
            // This is the magic bullet. The connection usually establishes after 
            // the scan results callback so release the wake lock here.
            if(netInfo.isConnected()) {
                mWakeLock.release();
            }
        }
        //Search the scan results for saved WiFi APs.
        else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action))
        {
            boolean foundMatch = false;
            if (!IsWiFiConnected()) {
                Map<String, Integer> savedNetworks = new HashMap<String, Integer>();
                for (WifiConfiguration config : WiFi_Mgr.getConfiguredNetworks()) {
                    String escapedSSID = config.SSID.replaceAll("\"", "");
                    savedNetworks.put(escapedSSID, config.networkId);
                }
                List<ScanResult> scanResults = WiFi_Mgr.getScanResults();
                for (ScanResult ap : scanResults) {
                    Integer networkId = savedNetworks.get(ap.SSID);
                    if (networkId != null) {
                        savedNetworks.remove(ap.SSID);
                        WiFi_Mgr.enableNetwork(networkId, false);
                        foundMatch = true;
                    }
                }
            }
            if(foundMatch) {
                WiFi_Mgr.reassociate();
                WiFi_Mgr.reconnect();
            }
            if (IsWiFiConnected())
                mWakeLock.release();
        }
    }
};

You'll need to declare the necessary variables (e.g. mWakeLock is a partial, non-reference counted wakelock; WiFi_Mgr is an instance of WifiManager; etc...).

Distinctive answered 25/4, 2014 at 20:41 Comment(2)
We have not run into any issues using our solution. We tried a couple of 4.4 device (not a Nexus 4 though) and everything still works as expected. Not going to modify anything right now, but will be keep your findings in mind. Thanks.Helse
I found that getConfiguredNetworks() and getScanResults() can both return null. It's rare but it does happen. Just add a check for null for both of those and you're good.Distinctive
H
0

Had a second go around in the area. While the above solution did work for all our qualified devices, there were too many calls that might have been unnecessary. Plus we got new device for which the solution did not work. Here is a much better solution:

At every interval this code is called

NetworkInfo wifiInfo = _androidConnectivityMgr.GetNetworkInfo(ConnectivityType.Wifi);
if (!wifiInfo.IsConnectedOrConnecting)
{
    // Need to make sure the CPU does not go to sleep before the following async calls are finished
    _wifiScanWakeLock.Acquire();

    // Do not wait for the OS to initiate a reconnect to a Wi-Fi router
    _wifiManager.StartScan();
}
  • _wifiScanWakeLock is just a partial, non-reference counted WakeLock, Dispose in OnDestroy

When the Wi-Fi scan is finished

private void OnWifiScanResultsReceived(Intent result)
{
    NetworkInfo wifiInfo = _androidConnectivityMgr.GetNetworkInfo(ConnectivityType.Wifi);
    if (!wifiInfo.IsConnectedOrConnecting)
    {
        Dictionary<string, int> savedNetworks = new Dictionary<string, int>();
        foreach (WifiConfiguration config in _wifiManager.ConfiguredNetworks)
        {
            string escapedSsid = Regex.Replace(config.Ssid, "^\"|\"$", String.Empty);
            savedNetworks[escapedSsid] = config.NetworkId;
        }

        foreach (ScanResult ap in _wifiManager.ScanResults)
        {
            int networkId;
            if (savedNetworks.TryGetValue(ap.Ssid, out networkId))
            {
                savedNetworks.Remove(ap.Ssid);
                _wifiManager.EnableNetwork(networkId, false);
            }
        }
    }
    _wifiScanWakeLock.Release();
}
  • BSSID for the WifiConfiguration is always null and cannot be used that to uniquely compare to the BSSID of the ScanResult
  • This is the core code, obviously you would have to worry about case of two identical SSIDs and other optimizations
Helse answered 18/3, 2014 at 15:52 Comment(2)
This is a really important question for my company, thanks for all your hard work and good documentation on this. Do you remember what event was triggering the above code? It's one thing to initiate a scan when the OS is kind enough to run you, but I'm finding partial wake lock + wifi lock + battery optimization exemption = still not enough to keep running when phone goes into full Doze (IDLE in ADB's BatteryStats). I've got a bounty out for this issue, if you look up my questions--I'd be happy to pay it out!Judy
Code above is triggered by the registering for the SCAN_RESULTS_AVAILABLE_ACTION intent, its in C# which has different format. Thats an interesting case, we also disabled doze mode for our application. I think one of the reasons we never ran into this case is because on connection loss we turn on the screen and notify the user which resets the doze mode counter. In our case users are not expected to be completely out of coverage for more than an hour. Would be interested to test this during a less busy period. As a bad solution, you could brute force turn on screen when doze mode is activated.Helse

© 2022 - 2024 — McMap. All rights reserved.