Android WiFiManager enableNetwork returning false
Asked Answered
E

1

8

TO BE CLEAR:

The most likely part of the code which has the problem is the connect function, which you can find in the code block.

EDIT:

I've had a good dig through LogCat and found something interesting (this occurred the exact moment enableNetwork was called):

2018-12-04 20:13:14.508 1315-7000/? I/WifiService: enableNetwork uid=10158 disableOthers=true
2018-12-04 20:13:14.508 1315-1607/? D/WifiStateMachine: connectToUserSelectNetwork netId 49, uid 10158, forceReconnect = false
2018-12-04 20:13:14.541 1315-1607/? D/WifiConfigStore: Writing to stores completed in 14 ms.
2018-12-04 20:13:14.541 1315-1607/? E/WifiConfigManager: UID 10158 does not have permission to update configuration "SKYD7F55"WPA_PSK
2018-12-04 20:13:14.541 1315-1607/? I/WifiStateMachine: connectToUserSelectNetwork Allowing uid 10158 with insufficient permissions to connect=49

PERMISSIONS:

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

I've been tasked to create a part of an app where the user will be able to see a list of scanned WiFi access points (ScanResult), be able to select one, and, if it requires auth, be presented with a screen where they enter the PSK. After entering the PSK, the system will attempt to connect to the access point by first creating and configuring a WifiConfig object, using addNetwork to add the config to the Wifi config table, then disconnect, enableNetwork and reconnect (in that order).

I'm using RX-Java2 so that I can chain the various steps of the network setup. For instance, the disconnect method returns a Completable which emits a completed event if WifiManager.disconnect() succeeds. It does so by registering a BroadcastReceiver to listen for NETWORK_STATE_CHANGED_ACTION and then emitting a completion event if the networkInfo extra has detailed state DISCONNECTED. The same logic applies for the connect() function.

Now, addNetwork() is succeeding (so my WiFi config functions are correct), and then I am chaining disconnect Completable to the connect Single using andThen. I have put breakpoints in my code and can see that everything is running in the correct order, disconnect is succeeding and then has it's broadcast receiver registered successfully, but enableNetwork() call is returning false (indicating that the enableNetwork command failed to be issued by the OS).

I am 99% sure this is not an issue with how I'm using RX, but, given that addNetwork and disconnect are succeeding (indicating my wifi config creation code is fine) I am starting to wonder if either A) My RX code is wrong, or, B) My WiFi config creation is wrong.

Therefore, I am going to post all the code below as well as the use case and would greatly appreciate any advice.

Emitters.kt:

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.NetworkInfo
import android.net.wifi.SupplicantState
import android.net.wifi.WifiConfiguration
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import com.google.common.base.Optional
import io.reactivex.*
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import java.lang.Exception

private const val TAG = "Emitters"
private const val SIGNAL_STRENGTH_RANGE = 4

/**
 * Use case of these emitters (in presenter or interactor):
 *
 * // If network is open, then go ahead and connect to it, else show enter password form
 * isNetworkOpen(context, scanResult).flatMapCompletable { isNetworkOpen ->
 *     if (isNetworkOpen) {
 *         connectToOpenWifi(context, scanResult.ssid, scanResult.bssid)
 *     } else {
 *         Completable.error(WifiException("The specified network requires a password")
 *     }
 * }.subscribeOn(Schedulers.io())
 *  .observeOn(AndroidSchedulers.mainThread())
 *  .subscribe({
 *      view?.showSuccessfullyConnected()
 *  }, { error ->
 *         when (error) {
 *             is WifiAuthException -> {
 *                  val auth = error.wifiScanResult.auth
 *                  val keyManagement = error.wifiScanResult.keyManagement
 *                  val security = "$auth/$keyManagement"
 *                  viewStateStack.add(NetworkSummaryViewState(error.wifiScanResult, security))
 *                  switchToViewState(viewStateStack.peek())
 *              } else -> {
 *                  viewStateStack.add(FailedToConnectViewState(networkName))
 *                  switchToViewState(viewStateStack.peek())
 *              }
 *          }
 *      }
 *  })
 *
 *  // Called by view to connect to closed network with provided password
 *  connectToClosedWifi(context, scanResult, password)
 *  .subscribeOn(Schedulers.io())
 *  .observeOn(AndroidSchedulers.mainThread())
 *  .subscribe({
 *      view?.showSuccessfullyConnected()
 *  }, { error ->
 *      view?.showFailedToConnect()
 *  })
 */

/**
 * Creates a Flowable that emits WiFiScanResults
 */
fun wifiScanResults(context: Context): Flowable<Set<WiFiScanResult>> = Flowable.create<Set<WiFiScanResult>> ({ emitter ->
    val wifiManagerWrapper = WifiManagerWrapper(context.applicationContext)

    if (!wifiManagerWrapper.wifiManager.isWifiEnabled && !wifiManagerWrapper.wifiManager.setWifiEnabled(true)) {
        wifiManagerWrapper.dispose()
        emitter.onError(WiFiException("WiFi not enabled and couldn't enable it"))
        return@create
    }

    // Broadcast receiver that handles wifi scan results
    val wifiScanReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) {
                val scanResults = wifiManagerWrapper.wifiManager.scanResults

                if (scanResults !== null) {
                    emitter.onNext(scanResults.map { scanResult ->
                        val signalStrength = WifiManager.calculateSignalLevel(scanResult.level, SIGNAL_STRENGTH_RANGE)

                        val capabilities = scanResult.capabilities.substring(1, scanResult.capabilities.indexOf(']') -1)
                            .split('-')
                            .toSet()

                        WiFiScanResult(scanResult.SSID,
                            scanResult.BSSID,
                            capabilities.elementAtOrNull(0) ?: "",
                            capabilities.elementAtOrNull(1) ?: "",
                            capabilities.elementAtOrNull(2) ?: "",
                            signalStrength)

                    }.toSet())
                }
            }

            if (!wifiManagerWrapper.wifiManager.startScan()) {
                emitter.onError(WiFiException("WiFi not enabled"))
            }
        }
    }

    val wifiScanResultsIntentFilter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION)
    context.applicationContext.registerReceiver(wifiScanReceiver, wifiScanResultsIntentFilter)

    emitter.setCancellable {
        context.unregisterReceiver(wifiScanReceiver)
        wifiManagerWrapper.dispose()
    }

    if (!wifiManagerWrapper.wifiManager.startScan()) {
        emitter.onError(WiFiException("WiFi not enabled"))
    }
}, BackpressureStrategy.LATEST).subscribeOn(Schedulers.io())

/**
 * Returns a single indicating if the [scanResult] is open
 */
fun isNetworkOpen(context: Context,
                  scanResult: WiFiScanResult): Single<Boolean> = Single.create<Boolean> { emitter ->
    val wifiManagerWrapper = WifiManagerWrapper(context.applicationContext)

    emitter.setCancellable {
        wifiManagerWrapper.dispose()
    }

    if (scanResult.auth.contains("WEP")) {
        emitter.onSuccess(true)
    } else {
        emitter.onSuccess(false)
    }
}

/**
 * Attempts to connect to an open wifi access point specified by [scanResult]
 * Emits a completed event if successful, else emits an error
 */
fun connectToOpenWifi(context: Context,
                      scanResult: WiFiScanResult): Completable = Completable.create { emitter ->
    val ssid = scanResult.ssid
    val bssid = scanResult.bssid

    val wifiManagerWrappper = WifiManagerWrapper(context.applicationContext)

    if (!wifiManagerWrappper.wifiManager.isWifiEnabled && !wifiManagerWrappper.wifiManager.setWifiEnabled(true)) {
        wifiManagerWrappper.dispose()
        emitter.onError(WiFiException("Wifi not enabled"))
    }

    val updateWifiStateObs = getExistingConfiguration(wifiManagerWrappper.wifiManager, ssid, bssid).flatMap { existingConfig ->
        if (!existingConfig.isPresent) {
            createOpenWifiConfiguration(scanResult).flatMap { wifiConfig ->
                val newNetworkId = wifiManagerWrappper.wifiManager.addNetwork(wifiConfig)
                if (newNetworkId < 0)
                    throw WiFiException("Failed to add new access point ${scanResult.ssid}")

                val currentWifiConnection = wifiManagerWrappper.wifiManager.connectionInfo

                if (currentWifiConnection !== null) {
                    disconnect(context, wifiManagerWrappper.wifiManager).andThen(
                        connect(context, wifiManagerWrappper.wifiManager, wifiConfig.SSID, newNetworkId)
                    )
                } else {
                    connect(context, wifiManagerWrappper.wifiManager, wifiConfig.SSID, newNetworkId)
                }
            }
        } else {
            Single.just(existingConfig.get())
        }
    }

    val compositeDisposable = CompositeDisposable()

    emitter.setCancellable {
        compositeDisposable.clear()
        wifiManagerWrappper.dispose()
    }

    try {
        compositeDisposable.add(updateWifiStateObs.subscribe({
            emitter.onComplete()
        }, { error ->
            emitter.onError(error)
        }))
    } catch (ex: Exception) {
        compositeDisposable.clear()
        wifiManagerWrappper.dispose()
        emitter.onError(ex)
    }
}

/**
 * Attempts to connect to an closed [scanResult] by providing the given [preSharedKey]
 * Emits a completed event if successful, else emits an error
 */
fun connectToClosedWifi(context: Context,
                        scanResult: WiFiScanResult,
                        preSharedKey: String): Completable = Completable.create { emitter ->
    val ssid = scanResult.ssid
    val bssid = scanResult.bssid

    val wifiManagerWrappper = WifiManagerWrapper(context.applicationContext)

    if (!wifiManagerWrappper.wifiManager.isWifiEnabled && !wifiManagerWrappper.wifiManager.setWifiEnabled(true)) {
        wifiManagerWrappper.dispose()
        emitter.onError(WiFiException("Wifi not enabled"))
    }

    val updateWifiStateObs =
        getExistingConfiguration(wifiManagerWrappper.wifiManager, ssid, bssid).flatMap { existingConfig ->
            if (!existingConfig.isPresent) {
                createClosedWifiConfiguaration(scanResult, preSharedKey).flatMap { wifiConfig ->
                    val newNetworkId = wifiManagerWrappper.wifiManager.addNetwork(wifiConfig)
                    if (newNetworkId < 0)
                        throw WiFiException("Failed to add new access point ${scanResult.ssid}")

                    val currentWifiConnection = wifiManagerWrappper.wifiManager.connectionInfo

                    if (currentWifiConnection !== null) {
                        disconnect(context, wifiManagerWrappper.wifiManager).andThen(
                            connect(context, wifiManagerWrappper.wifiManager, wifiConfig.SSID, newNetworkId)
                        )
                    } else {
                        connect(context, wifiManagerWrappper.wifiManager, wifiConfig.SSID, newNetworkId)
                    }
                }
            } else {
                Single.just(existingConfig.get())
            }
        }

    val compositeDisposable = CompositeDisposable()

    emitter.setCancellable {
        compositeDisposable.clear()
        wifiManagerWrappper.dispose()
    }

    try {
        compositeDisposable.add(updateWifiStateObs.subscribe({
            emitter.onComplete()
        }, { error ->
            emitter.onError(error)
        }))
    } catch (ex: Exception) {
        compositeDisposable.clear()
        wifiManagerWrappper.dispose()
        emitter.onError(ex)
    }
}

/**
 * Wrapper class for WiFiManager that creates a multicast lock to make the app handle multicast wifi packets
 * Handles disposing of the lock and cleaning up of resources via the dispose method
 */
private class WifiManagerWrapper(context: Context) {
    val wifiManager = context.applicationContext.getSystemService(Context.WIFI_SERVICE)
            as? WifiManager ?: throw IllegalStateException("Could not get system Context.WIFI_SERVICE")

    // Create and acquire a multicast lock to start receiving multicast wifi packets
    private var lock = wifiManager.createMulticastLock(TAG + "_lock").apply {
        acquire()
    }

    // Dispose of the lock
    fun dispose() {
        if (lock.isHeld) {
            try {
                lock.release()
            } catch (ignore: Exception) {
                EventReporter.i(TAG, "Failed to release lock on wifi manager wrapper")
            }
            lock = null
        }
    }
}

/**
 * Disconnects from the connected wifi network and emits a completed event if no errors occurred
 */
private fun disconnect(context: Context,
                       wifiManager: WifiManager) = Completable.create { emitter ->

    val wifiDisconnectionReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == WifiManager.NETWORK_STATE_CHANGED_ACTION) {
                val networkInfo = intent.getParcelableExtra<NetworkInfo>(WifiManager.EXTRA_NETWORK_INFO) ?: return
                if (networkInfo.detailedState == NetworkInfo.DetailedState.DISCONNECTED) {
                    context.applicationContext.unregisterReceiver(this)
                    emitter.onComplete()
                }
            }
        }
    }

    val networkStateChangedFilter = IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION)

    context.applicationContext.registerReceiver(wifiDisconnectionReceiver, networkStateChangedFilter)

    emitter.setCancellable {
        if (!emitter.isDisposed)
            context.applicationContext.unregisterReceiver(wifiDisconnectionReceiver)
    }

    if (!wifiManager.disconnect())
        emitter.onError(WiFiException("Failed to issue disconnect command to wifi subsystem"))
}

/**
 * Connects to the wifi access point at specified [ssid] with specified [networkId]
 * And returns the [WifiInfo] of the network that has been connected to
 */
private fun connect(context: Context,
                    wifiManager: WifiManager,
                    ssid: String,
                    networkId: Int) = Single.create<WifiInfo> { emitter ->

    val wifiConnectionReceiver = object : BroadcastReceiver() {
        var oldSupplicantState: SupplicantState? = null

        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == WifiManager.NETWORK_STATE_CHANGED_ACTION) {
                val networkInfo = intent.getParcelableExtra<NetworkInfo>(WifiManager.EXTRA_NETWORK_INFO) ?: return

                if (networkInfo.detailedState == NetworkInfo.DetailedState.DISCONNECTED) {
                    context.applicationContext.unregisterReceiver(this)
                    emitter.onError(WiFiException("Failed to connect to wifi network"))
                }
                else if (networkInfo.detailedState == NetworkInfo.DetailedState.CONNECTED) {
                    val wifiInfo = intent.getParcelableExtra<WifiInfo>(WifiManager.EXTRA_WIFI_INFO) ?: return
                    if (ssid == wifiInfo.ssid.unescape()) {
                        context.applicationContext.unregisterReceiver(this)
                        emitter.onSuccess(wifiInfo)
                    }
                }
            } else if (intent.action == WifiManager.SUPPLICANT_STATE_CHANGED_ACTION) {
                val supplicantState = intent.getParcelableExtra<SupplicantState>(WifiManager.EXTRA_NEW_STATE)
                val oldSupplicantState = this.oldSupplicantState
                this.oldSupplicantState = supplicantState

                if (supplicantState == SupplicantState.DISCONNECTED) {
                    if (oldSupplicantState == null || oldSupplicantState == SupplicantState.COMPLETED) {
                        return
                    }
                    val possibleError = intent.getIntExtra(WifiManager.EXTRA_SUPPLICANT_ERROR, -1)
                    if (possibleError == WifiManager.ERROR_AUTHENTICATING) {
                        context.applicationContext.unregisterReceiver(this)
                        emitter.onError(WiFiException("Wifi authentication failed"))
                    }
                } else if (supplicantState == SupplicantState.SCANNING && oldSupplicantState == SupplicantState.DISCONNECTED) {
                    context.applicationContext.unregisterReceiver(this)
                    emitter.onError(WiFiException("Failed to connect to wifi network"))
                }
            }
        }
    }

    val networkStateChangedFilter = IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION)
    networkStateChangedFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)

    context.applicationContext.registerReceiver(wifiConnectionReceiver, networkStateChangedFilter)

    emitter.setCancellable {
        if (!emitter.isDisposed)
            context.applicationContext.unregisterReceiver(wifiConnectionReceiver)
    }

    wifiManager.enableNetwork(networkId, true)
    wifiManager.reconnect()
}

/**
 * Returns a Single, wrapping an Optional.absent if no existing configuration exists with the passed [ssid] and [bssid], else the found [WifiConfiguration]
  */
private fun getExistingConfiguration(wifiManager: WifiManager,
                                     ssid: String,
                                     bssid: String) = Single.create<Optional<WifiConfiguration>> { emitter ->
    val configuredNetworks = wifiManager.configuredNetworks
    if (configuredNetworks.isEmpty()) {
        emitter.onSuccess(Optional.absent())
    }
    emitter.onSuccess(Optional.fromNullable(configuredNetworks.firstOrNull { configuredNetwork ->
        configuredNetwork.SSID.unescape() == ssid && configuredNetwork.BSSID == bssid
    }))
}

/**
 * Emits a single of the open [WifiConfiguration] created from the passed [scanResult]
 */
private fun createOpenWifiConfiguration(scanResult: WiFiScanResult) = Single.fromCallable<WifiConfiguration> {
    val auth = scanResult.auth
    val keyManagement = scanResult.keyManagement
    val pairwiseCipher = scanResult.pairwiseCipher

    val config = WifiConfiguration()
    config.SSID = "\"" +  scanResult.ssid + "\""
    config.BSSID = scanResult.bssid

    var allowedProtocols = 0
    when {
        auth.isEmpty() -> {
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.RSN
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.WPA
        }
        auth.contains("WPA2") -> allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.RSN
        auth.contains("WPA") -> {
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.WPA
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.RSN
        }
    }

    config.allowedProtocols.set(allowedProtocols)

    var allowedAuthAlgos = 0
    when {
        auth.contains("EAP") -> allowedAuthAlgos = allowedAuthAlgos or WifiConfiguration.AuthAlgorithm.LEAP
        auth.contains("WPA") -> allowedAuthAlgos = allowedAuthAlgos or WifiConfiguration.AuthAlgorithm.OPEN
        auth.contains("WEP") -> allowedAuthAlgos = allowedAuthAlgos or WifiConfiguration.AuthAlgorithm.SHARED
    }

    config.allowedAuthAlgorithms.set(allowedAuthAlgos)

    var allowedKeyManagers = WifiConfiguration.KeyMgmt.NONE
    if (keyManagement.contains("IEEE802.1X"))
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.IEEE8021X
    else if (auth.contains("WPA") && keyManagement.contains("EAP"))
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.WPA_EAP
    else if (auth.contains("WPA") && keyManagement.contains("PSK"))
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.WPA_PSK

    config.allowedKeyManagement.set(allowedKeyManagers)

    var allowedPairWiseCiphers = WifiConfiguration.PairwiseCipher.NONE
    if (pairwiseCipher.contains("CCMP"))
        allowedPairWiseCiphers = allowedPairWiseCiphers or WifiConfiguration.PairwiseCipher.CCMP
    if (pairwiseCipher.contains("TKIP"))
        allowedPairWiseCiphers = allowedPairWiseCiphers or WifiConfiguration.PairwiseCipher.TKIP

    config.allowedPairwiseCiphers.set(allowedPairWiseCiphers)

    config
}

/**
 * Emits a single of the closed [WifiConfiguration] created from the passed [scanResult] and [preSharedKey]
 * Or, emits an error signalling the [preSharedKey] was empty
 */
private fun createClosedWifiConfiguaration(scanResult: WiFiScanResult, preSharedKey: String) = Single.fromCallable<WifiConfiguration> {
    val auth = scanResult.auth
    val keyManagement = scanResult.keyManagement
    val pairwiseCipher = scanResult.pairwiseCipher

    val config = WifiConfiguration()
    config.SSID = "\"" +  scanResult.ssid + "\""
    config.BSSID = scanResult.bssid

    var allowedProtocols = 0
    when {
        auth.isEmpty() -> {
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.RSN
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.WPA
        }
        auth.contains("WPA2") -> allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.RSN
        auth.contains("WPA") -> {
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.WPA
            allowedProtocols = allowedProtocols or WifiConfiguration.Protocol.RSN
        }
    }

    config.allowedProtocols.set(allowedProtocols)

    var allowedAuthAlgos = 0
    when {
        auth.contains("EAP") -> allowedAuthAlgos = allowedAuthAlgos or WifiConfiguration.AuthAlgorithm.LEAP
        auth.contains("WPA") || auth.contains("WPA2") -> allowedAuthAlgos = allowedAuthAlgos or WifiConfiguration.AuthAlgorithm.OPEN
        auth.contains("WEP") -> allowedAuthAlgos = allowedAuthAlgos or WifiConfiguration.AuthAlgorithm.SHARED
    }

    config.allowedAuthAlgorithms.set(allowedAuthAlgos)

    var allowedKeyManagers = WifiConfiguration.KeyMgmt.NONE
    if (keyManagement.contains("IEEE802.1X"))
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.IEEE8021X
    else if (auth.contains("WPA") && keyManagement.contains("EAP"))
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.WPA_EAP
    else if (auth.contains("WPA") && keyManagement.contains("PSK"))
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.WPA_PSK
    else if (preSharedKey.isNotEmpty())
        allowedKeyManagers = allowedKeyManagers or WifiConfiguration.KeyMgmt.WPA_PSK
    else if (preSharedKey.isEmpty())
        allowedKeyManagers = allowedAuthAlgos or WifiConfiguration.KeyMgmt.WPA_PSK

    config.allowedKeyManagement.set(allowedKeyManagers)

    var allowedPairWiseCiphers = WifiConfiguration.PairwiseCipher.NONE
    if (pairwiseCipher.contains("CCMP"))
        allowedPairWiseCiphers = allowedPairWiseCiphers or WifiConfiguration.PairwiseCipher.CCMP
    if (pairwiseCipher.contains("TKIP"))
        allowedPairWiseCiphers = allowedPairWiseCiphers or WifiConfiguration.PairwiseCipher.TKIP

    config.allowedPairwiseCiphers.set(allowedPairWiseCiphers)

    if (preSharedKey.isNotEmpty()) {
        if (auth.contains("WEP")) {
            if (preSharedKey.matches("\\p{XDigit}+".toRegex())) {
                config.wepKeys[0] = preSharedKey
            } else {
                config.wepKeys[0] = "\"" + preSharedKey + "\""
            }
            config.wepTxKeyIndex = 0
        } else {
            config.preSharedKey = "\"" + preSharedKey + "\""
        }
    }

    config
}

/**
 * Extension function to remove escaped " from a string
 */
private fun String.unescape() =
    if (this.startsWith("\""))
        this.replace("\"", "")
    else
        this
Emmet answered 4/12, 2018 at 19:36 Comment(7)
Which device are you testing with? And what Android version? Later versions will require a run time permissions dialog for the user to grant some of those.Ilanailangilang
I've tested on a OnePlus 5 running 8.1.0. I have asked for runtime permission from user for COARSE_LOCATION and have granted it on my test device.Emmet
Have also tested on a rooted Samsung S7 also running Android 8.1.0Emmet
You might want to check this answer. https://mcmap.net/q/102582/-android-wifimanager-addnetwork-returns-1Norvin
Also, we found that Android's saved configurations do not always store the BSSID. You might consider changing this code into just an SSID check: configuredNetwork.SSID.unescape() == ssid && configuredNetwork.BSSID == bssid to configuredNetwork.SSID.unescape() == ssid ---- Side Note #2 I noticed that you do not listen for SupplicantState.ASSOCIATING. True connection/disconnection exists only when you are no longer associating.Norvin
I'll give it a try, I had noticed that when I retrieved my added WifiConfig object from the WifiManager.configuredNetworks after calling addNetwork the BSSID was 00:00:00:00:00:00:00 which I think indicates it was not saved.Emmet
Weirdly though, if I call macAddress I get the valid mac address back, I was under the impression that BSSID and Mac Address are interchangeable terms?Emmet
E
12

Ok, I've finally figured this out and I hope that my answer here sheds some light for anyone in the future who encounters a similar problem, because this was nasty and caused me quite the headache.

The root cause of the issue was that I had incorrectly configure the WiFiConfig object which was registered in the WiFiConfig table via WiFiConfigManager.addNetwork().

I had made a massive assumption about the contract of WifiConfigManager.addNetwork(). I had assumed that if that operation succeeded (i.e. did NOT return -1) then the passed WiFiConfig was configured correctly. This assumption is incorrect, the allowedAuthAlgorithms, allowedProtocols, allowedKeyManagers and allowedPairwiseCipher BitSet on the WiFiConfig I was creating were incorrect, yet the call to addNetwork() succeeded. I believe this is because the call to addNetwork() does not actually do anything other than validate that the config is valid to put in the WiFiConfig table, which is quite different than validating if it is the correct config for a given WiFi access point. This is backed up by the comments in the source code for addNetwork() which do NOT state the delivery of asynchronous state like a lot of the other WiFiManager functions, indicating (to me at least) that no attempt to communicate with the access point was made by the OS as a result of calling addNetwork().

Due to a very helpful suggestion by a colleague to connect to the access point in question via the OS, and then to compare the OS created WiFiConfig object for that access point with the one generated by my own code for discrepancies I noticed that my WiFiConfig was being configured incorrectly. It was shortly after this that I resolved the original question.

Now, why was my WiFiConfig object being created incorrectly? That is because I had little knowledge of how to configure WiFi (i.e. the various terminology and the meaning behind all the protocols, algorithms and key managers). So, after reading the official docs and not gleaning much helpful information I turned to StackOverflow questions and answers and found a recurring pattern for setting the WiFiConfig up correctly, they all appeared to use BitWise operators to create an Int value which was ultimately passed to the WiFiConfig.allowedProtocols.set(), WiFiConfig.allowedPairwiseCiphers.set(), WiFiConfig.allowedKeyManagement.set() and WiFiConfig.allowedAuthAlgorithm.set() functions.

It turns out that the underlying BitSet for each of those configuration options is a data structure which maintains a dynamically resizing vector of bits, where the index of a bit in a given BitSet instance in the WiFiConfig object implicitly corresponded to the index of an element in an implicitly associated String array within the WiFiConfig object. Therefore, if you wished to provide multiple protocols, keyManagements, pairwiseCiphers or authAlgorithms you would need to call set on the underlying corresponding BitSet, passing in the correct index which would correspond to the element of the implicitly associated String array which matched the chosen protocol.

After re-writing my WiFiConfig creation code, the issue resolved itself. Although there was a bug in my code in the original post which has also been fixed.

Here is the new WiFiConfig creation code:

/**
 * Emits a single of the [WifiConfiguration] created from the passed [scanResult] and [preSharedKey]
 */
private fun createWifiConfiguration(scanResult: WiFiScanResult, preSharedKey: String) = Single.fromCallable<WifiConfiguration> {
    val auth = scanResult.auth
    val keyManagement = scanResult.keyManagement
    val pairwiseCipher = scanResult.pairwiseCipher

    val config = WifiConfiguration()
    config.SSID = "\"" +  scanResult.ssid + "\""
    config.BSSID = scanResult.bssid

    if (auth.contains("WPA") || auth.contains("WPA2")) {
        config.allowedProtocols.set(WifiConfiguration.Protocol.WPA)
        config.allowedProtocols.set(WifiConfiguration.Protocol.RSN)
    }

    if (auth.contains("EAP"))
        config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.LEAP)
    else if (auth.contains("WPA") || auth.contains("WPA2"))
        config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN)
    else if (auth.contains("WEP"))
        config.allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED)

    if (keyManagement.contains("IEEE802.1X"))
        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X)
    else if (auth.contains("WPA") && keyManagement.contains("EAP"))
        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP)
    else if (auth.contains("WPA") && keyManagement.contains("PSK"))
        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)
    else if (auth.contains("WPA2") && keyManagement.contains("PSK"))
        config.allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK)

    if (pairwiseCipher.contains("CCMP") || pairwiseCipher.contains("TKIP")) {
        config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.CCMP)
        config.allowedPairwiseCiphers.set(WifiConfiguration.PairwiseCipher.TKIP)
    }

    if (preSharedKey.isNotEmpty()) {
        if (auth.contains("WEP")) {
            if (preSharedKey.matches("\\p{XDigit}+".toRegex())) {
                config.wepKeys[0] = preSharedKey
            } else {
                config.wepKeys[0] = "\"" + preSharedKey + "\""
            }
            config.wepTxKeyIndex = 0
        } else {
            config.preSharedKey = "\"" + preSharedKey + "\""
        }
    }

    config
}

And here is the new connect code:

/**
 * Connects to the wifi access point at specified [ssid] with specified [networkId]
 * And returns the [WifiInfo] of the network that has been connected to
 */
private fun connect(context: Context,
                    wifiManager: WifiManager,
                    ssid: String,
                    networkId: Int) = Single.create<WifiInfo> { emitter ->

    val wifiConnectionReceiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == WifiManager.NETWORK_STATE_CHANGED_ACTION) {
                val networkInfo = intent.getParcelableExtra<NetworkInfo>(WifiManager.EXTRA_NETWORK_INFO) ?: return

                if (networkInfo.detailedState == NetworkInfo.DetailedState.CONNECTED) {
                    val wifiInfo = intent.getParcelableExtra<WifiInfo>(WifiManager.EXTRA_WIFI_INFO) ?: return
                    if (ssid.unescape() == wifiInfo.ssid.unescape()) {
                        context.applicationContext.unregisterReceiver(this)
                        emitter.onSuccess(wifiInfo)
                    }
                }
            }
        }
    }

    val networkStateChangedFilter = IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION)
    networkStateChangedFilter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION)

    context.applicationContext.registerReceiver(wifiConnectionReceiver, networkStateChangedFilter)

    emitter.setCancellable {
        if (!emitter.isDisposed)
            context.applicationContext.unregisterReceiver(wifiConnectionReceiver)
    }

    wifiManager.enableNetwork(networkId, true)
}
Emmet answered 12/12, 2018 at 18:32 Comment(2)
So you know when the network is truly connected (BroadcastReceiver is triggered) , but how can I decide it didn't succeed to connect?Active
In the broadcast receiver, you can add else cases to the if statement to handle the different states. You probably want to check NetworkInfo.DetailedState.FAILED. However, the NetworkInfo class is deprecated, so proceed with caution.Emmet

© 2022 - 2024 — McMap. All rights reserved.