Android WiFiManager enableNetwork returning false - android

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

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)
}

Related

What are the steps to receive notification data using BLE in android from a characteristic that is written to?

I have a characteristic that when written to the BLE device I am attempting to communicate with is supposed to respond with certain data.
When I enabled notification on the characteristic that is used when sending data, the onCharacteristicChanged function was called. But when I enabled notifications on the characteristic that I am supposed to use when receiving data, it never gets called.
My app is supposed to communicate with the BGX ble kit. The documentation here: https://docs.silabs.com/gecko-os/1/bgx/latest/ble-services
According to the link above in order to transmit data to the kit we use the RX characteristic where as to receive we use to TX characteristic.
private val XpressStreamingServiceUUID = UUID.fromString("331a36f5-2459-45ea-9d95-6142f0c4b307")
private val peripheralRX = UUID.fromString("a9da6040-0823-4995-94ec-9ce41ca28833")
private val peripheralTX = UUID.fromString("a73e9a10-628f-4494-a099-12efaf72258f")
If I enable notifications on the RX I do get notified but the same never happens to the TX. To elaborate there is a Processor Board we are using that is connected to the BLE kit. So basically if I send data it is to reach the board. The Write commands have been verified to reach their destination. So transmitting data from my app as a payload works fine.
Now I am supposed to send requests to the BLE device and receive a response in return. When it comes to sending, these functions come into play:
fun write(message:String){
val bytes = BigInteger(message.replace("\\s".toRegex(), ""), 16).toByteArray()
Timber.i("Bytes value ---> ${bytes.toHexString()}")
val device = getBleDevice()
// val characteristicRX = getBleCharacteristic()
val characteristicRX = bluetoothGattRef.getService(XpressStreamingServiceUUID).getCharacteristic(
peripheralRX)
writeCharacteristic(device, characteristicRX, bytes)
}
fun requestReadValues(requestCode:String){
if(isConnected.value!!){
requestData(requestCode)
}else{
Timber.e("Make sure that you connected and paired with the desired device.")
}
}
fun sendMessage(message:String){
Timber.i("Check if isConnected = true --> ${isConnected.value}")
if(isConnected.value == true){
write(message)
}else{
Timber.e("Make sure that you connected and paired with the desired device.")
}
}
fun requestData(data: String) {
val bytes = BigInteger(data.replace("\\s".toRegex(), ""), 16).toByteArray()
Timber.i("Bytes value of request---> ${bytes.toHexString()}")
val device = getBleDevice()
val characteristic = bluetoothGattRef.getService(XpressStreamingServiceUUID).getCharacteristic(peripheralRX)
writeCharacteristic(device, characteristic, bytes)
}
These are all in a BLEConnectionManager Class. Which as the name suggests is used to manage connections with BLE devices.
I have a queuing mechanism in place like this:
#Synchronized
private fun enqueueOperation(operation: BleOperationType) {
operationQueue.add(operation)
if (pendingOperation == null) {
doNextOperation()
}
}
#Synchronized
private fun signalEndOfOperation() {
Timber.d("End of $pendingOperation")
pendingOperation = null
if (operationQueue.isNotEmpty()) {
doNextOperation()
}
}
#Synchronized
private fun doNextOperation() {
if (pendingOperation != null) {
Timber.e("doNextOperation() called when an operation is pending! Aborting.")
return
}
val operation = operationQueue.poll() ?: run {
Timber.v("Operation queue empty, returning")
return
}
pendingOperation = operation
// Handle Connect separately from other operations that require device to be connected
if (operation is Connect) {
with(operation) {
Timber.w("Connecting to ${device.name} | ${device.address}")
device.connectGatt(context, false, gattCallback)
isConnected.value = true
}
return
}
// Check BluetoothGatt availability for other operations
val gatt = deviceGattMap[operation.device]
?: this#BleConnectionManager.run {
Timber.e("Not connected to ${operation.device.address}! Aborting $operation operation.")
signalEndOfOperation()
return
}
// TODO: Make sure each operation ultimately leads to signalEndOfOperation()
// TODO: Refactor this into an BleOperationType abstract or extension function
when (operation) {
is Disconnect -> with(operation) {
Timber.w("Disconnecting from ${device.address}")
gatt.close()
deviceGattMap.remove(device)
listeners.forEach { it.get()?.onDisconnect?.invoke(device) }
signalEndOfOperation()
setConnectionStatus(STATE_DISCONNECTED)
isConnected.value = false
}
is CharacteristicWrite -> with(operation) {
gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
characteristic.writeType = writeType
characteristic.value = payLoad
gatt.writeCharacteristic(characteristic)
} ?: this#BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID to write to")
signalEndOfOperation()
}
}
is CharacteristicRead -> with(operation) {
gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
gatt.readCharacteristic(characteristic)
} ?: this#BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID to read from")
signalEndOfOperation()
}
}
is DescriptorWrite -> with(operation) {
gatt.findDescriptor(descriptorUUID)?.let { descriptor ->
descriptor.value = payLoad
gatt.writeDescriptor(descriptor)
} ?: this#BleConnectionManager.run {
Timber.e("Cannot find $descriptorUUID to write to")
signalEndOfOperation()
}
}
is DescriptorRead -> with(operation) {
gatt.findDescriptor(descriptorUUID)?.let { descriptor ->
gatt.readDescriptor(descriptor)
} ?: this#BleConnectionManager.run {
Timber.e("Cannot find $descriptorUUID to read from")
signalEndOfOperation()
}
}
is EnableNotifications -> with(operation) {
gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
val cccdUuid = UUID.fromString(CCC_DESCRIPTOR_UUID)
val payload = when {
characteristic.isIndicatable() ->
BluetoothGattDescriptor.ENABLE_INDICATION_VALUE
characteristic.isNotifiable() ->
BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
else ->
error("${characteristic.uuid} doesn't support notifications/indications")
}
characteristic.getDescriptor(cccdUuid)?.let { cccDescriptor ->
if (!gatt.setCharacteristicNotification(characteristic, true)) {
Timber.e("setCharacteristicNotification failed for ${characteristic.uuid}")
signalEndOfOperation()
return
}
/** **/
else if(gatt.setCharacteristicNotification(characteristic, true)){
Timber.e("setCharacteristicNotification succeeded for ${characteristic.uuid}")
}
/** **/
cccDescriptor.value = payload
gatt.writeDescriptor(cccDescriptor)
} ?: this#BleConnectionManager.run {
Timber.e("${characteristic.uuid} doesn't contain the CCC descriptor!")
signalEndOfOperation()
}
} ?: this#BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID! Failed to enable notifications.")
signalEndOfOperation()
}
}
is DisableNotifications -> with(operation) {
gatt.findCharacteristic(characteristicUUID)?.let { characteristic ->
val cccdUuid = UUID.fromString(CCC_DESCRIPTOR_UUID)
characteristic.getDescriptor(cccdUuid)?.let { cccDescriptor ->
if (!gatt.setCharacteristicNotification(characteristic, false)) {
Timber.e("setCharacteristicNotification failed for ${characteristic.uuid}")
signalEndOfOperation()
return
}
cccDescriptor.value = BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(cccDescriptor)
} ?: this#BleConnectionManager.run {
Timber.e("${characteristic.uuid} doesn't contain the CCC descriptor!")
signalEndOfOperation()
}
} ?: this#BleConnectionManager.run {
Timber.e("Cannot find $characteristicUUID! Failed to disable notifications.")
signalEndOfOperation()
}
}
is MtuRequest -> with(operation) {
gatt.requestMtu(mtu)
}
}
}
This is my onCharacteristicChanged method:
override fun onCharacteristicChanged(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic
) {
with(characteristic) {
Timber.i("Characteristic $uuid changed | value: ${value.toHexString()}")
listeners.forEach { it.get()?.onCharacteristicChanged?.invoke(gatt.device, this) }
}
}
I also have a ConnectionEventListener in the activity or fragment that is used when sending data to detect certain events:
private val connectionEventListener by lazy {
ConnectionEventListener().apply {
onDisconnect = {
runOnUiThread {
alert {
title = "Disconnected"
message = "Disconnected from device."
positiveButton("OK") { onBackPressed() }
}.show()
if(STATE_DISCONNECTED == BleConnectionManager.getConnectionStatus()){
Timber.i("Connection Status: ${BleConnectionManager.getConnectionStatus()}, therefore disconnected, ${BleConnectionManager.isConnected.value}")
Timber.i("Device Disconnected from: ${BleConnectionManager.getBleDevice().name} | ${BleConnectionManager.getBleDevice().address}")
Toast.makeText(this#MainActivity,"Please turn on BLUETOOTH",Toast.LENGTH_SHORT).show()
}else if(STATE_DISCONNECTED != BleConnectionManager.getConnectionStatus()){
Timber.e("Connection Status: ${BleConnectionManager.getConnectionStatus()}, did not disconnect")
}
}
}
onConnectionSetupComplete = {
if (STATE_CONNECTED == BleConnectionManager.getConnectionStatus()) {
Timber.i("Connection Status: ${BleConnectionManager.getConnectionStatus()}, therefore connected, ${BleConnectionManager.isConnected.value}")
Timber.i("Device Connected to: ${BleConnectionManager.getBleDevice().name} | ${BleConnectionManager.getBleDevice().address}")
}
}
onCharacteristicChanged = { _, characteristic ->
Timber.i("Value changed on ${characteristic.uuid}: ${characteristic.value.toHexString()}")
val byteArray = ByteArray(characteristic.value.size)
System.arraycopy(characteristic.value, 0, byteArray, 0, characteristic.value.size)
Timber.i("Message Received: ${byteArray.toHexString()}")
}
onNotificationsEnabled = { _, characteristic ->
Timber.i("Notify enabled on: ${characteristic.uuid}")
}
}
}
I do have to note that a log message confirming that the TX characteristic notifications are enable dis only when the services are discovered. So basically when the onServiceDiscovered function is called the "enableNotifications" is indeed used, but that's it. I don't really receive any notifications and I don't know how to proceed.
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
with(gatt) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val services = gatt.services
Timber.w("Discovered ${services.size} services for ${device.address}.")
printGattTable()
requestMtu(device, GATT_MAX_MTU_SIZE)
val characteristic: BluetoothGattCharacteristic = this.getService(XpressStreamingServiceUUID).getCharacteristic(peripheralRX)
// this.setCharacteristicNotification(characteristic, true)
// setBleCharacteristic(characteristic)
// setNotification(gatt.getService(XpressStreamingServiceUUID).getCharacteristic(
// peripheralTX), true)
enableNotifications(device, characteristic)
// gatt.setCharacteristicNotification(characteristic, true)
// val desc = characteristic.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG_UUID)
// desc.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
// gatt.writeDescriptor(desc)
listeners.forEach { it.get()?.onConnectionSetupComplete?.invoke(this) }
} else {
Timber.e("Service discovery failed due to status $status")
teardownConnection(gatt.device)
disconnect()
}
}
if (pendingOperation is Connect) {
signalEndOfOperation()
}
}
This is a screenshot message of my log when notifications are enabled on the Tx characteristic:
What this shows is that the request was sent using the RX characteristic but nothing is received using the TX. Only that notifications were enabled on the TX.
UPDATE: new screenshots of my log

Android check internet connection in kotlin

I need to check if the device have internet connection, I search for some examples but when i copy and paste the code i always get errors or deprecated function. I also not understand where I have to put the method that check the connection, because I need to check the internet connection in the viewModel to make some request, and all the methods that i found have Context in the parameters, and I can't get Context in viewModel.
I try this code but I don't understand where I have to put it and I get
'TYPE_WIFI, TYPE_MOBILE, TYPE_ETHERNET: Int' is deprecated. Deprecated in Java
private fun isInternetAvailable(context: Context): Boolean {
var result = false
val connectivityManager =
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val networkCapabilities = connectivityManager.activeNetwork ?: return false
val actNw =
connectivityManager.getNetworkCapabilities(networkCapabilities) ?: return false
result = when {
actNw.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> true
actNw.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> true
actNw.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> true
else -> false
}
} else {
connectivityManager.run {
connectivityManager.activeNetworkInfo?.run {
result = when (type) {
ConnectivityManager.TYPE_WIFI -> true
ConnectivityManager.TYPE_MOBILE -> true
ConnectivityManager.TYPE_ETHERNET -> true
else -> false
}
}
}
}
return result
}
Someone can explain me how to make this check?
I created a helper class.
Network.kt
object Network {
private const val NETWORK_STATUS_NOT_CONNECTED = 0
private const val NETWORK_STATUS_WIFI = 1
private const val NETWORK_STATUS_MOBILE = 2
private const val TYPE_WIFI = 1
private const val TYPE_MOBILE = 2
private const val TYPE_NOT_CONNECTED = 0
private fun connectivityStatus(context: Context): Int {
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetwork = connectivityManager.activeNetworkInfo
if (null != activeNetwork) {
if (activeNetwork.type == ConnectivityManager.TYPE_WIFI) return TYPE_WIFI
if (activeNetwork.type == ConnectivityManager.TYPE_MOBILE) return TYPE_MOBILE
}
return TYPE_NOT_CONNECTED
}
private fun connectivityStatusString(context: Context): Int {
val connection = connectivityStatus(context)
var status = -1
if (connection == TYPE_WIFI) status = NETWORK_STATUS_WIFI else if (connection == TYPE_MOBILE) status = NETWORK_STATUS_MOBILE else if (connection == TYPE_NOT_CONNECTED) status = NETWORK_STATUS_NOT_CONNECTED
return status
}
fun checkConnectivity(context : Context):Boolean{
val status = connectivityStatusString(context)
return status == NETWORK_STATUS_WIFI || status == NETWORK_STATUS_MOBILE
}
}
and to access it you need to use it like this
if (Network.checkConnectivity(this#MainActivity))
\\Internet is working
else
\\No internet connectivity
You have to extend AndroidViewModel() class. After that you can reach application context in your viewModel.
class viewModel(app: Application): AndroidViewModel(app) {}

Detect 5G connection on Android phone

I'm trying to identify the type of cellular connection.
I've used different methods, like for example the one suggested here, but I keep getting 4G as a result, on a Samsung with Android 10 and 5G connection.
How is it possible to read the correct network type?
private fun getNetworkType(telephonyManager: TelephonyManager): String {
return when (telephonyManager.networkType) {
TelephonyManager.NETWORK_TYPE_UNKNOWN -> "unknown"
TelephonyManager.NETWORK_TYPE_GPRS,
TelephonyManager.NETWORK_TYPE_EDGE,
TelephonyManager.NETWORK_TYPE_CDMA,
TelephonyManager.NETWORK_TYPE_1xRTT,
TelephonyManager.NETWORK_TYPE_IDEN,
TelephonyManager.NETWORK_TYPE_GSM -> "2G"
TelephonyManager.NETWORK_TYPE_UMTS,
TelephonyManager.NETWORK_TYPE_EVDO_0,
TelephonyManager.NETWORK_TYPE_EVDO_A,
TelephonyManager.NETWORK_TYPE_HSDPA,
TelephonyManager.NETWORK_TYPE_HSUPA,
TelephonyManager.NETWORK_TYPE_HSPA,
TelephonyManager.NETWORK_TYPE_EVDO_B,
TelephonyManager.NETWORK_TYPE_EHRPD,
TelephonyManager.NETWORK_TYPE_HSPAP,
TelephonyManager.NETWORK_TYPE_TD_SCDMA -> "3G"
TelephonyManager.NETWORK_TYPE_LTE,
TelephonyManager.NETWORK_TYPE_IWLAN -> "4G"
TelephonyManager.NETWORK_TYPE_NR -> "5G"
else -> "something else"
}
}
private fun getRadioTechnology(telephonyManager: TelephonyManager): String {
try {
val obj = Class.forName(telephonyManager.javaClass.name)
.getDeclaredMethod("getServiceState", *arrayOfNulls(0)).invoke(telephonyManager, *arrayOfNulls(0))
val methods: Array<Method> = Class.forName(obj.javaClass.name).declaredMethods
for (method in methods) {
if (method.name == "getRadioTechnology" ) {
method.isAccessible = true
val radioTechnology = (method.invoke(obj) as Int).toInt()
return "$radioTechnology"
}
}
} catch (e: Exception) {
Log.e("Test5G", "", e)
}
return ""
}
#SuppressLint("MissingPermission")
fun getActiveSubscriptionInfoList(): String {
val subscriptionInfos = SubscriptionManager.from(this).activeSubscriptionInfoList
var ret: String = ""
for(sub in subscriptionInfos) {
val id = sub.subscriptionId
val telephonyManager = telephonyManager.createForSubscriptionId(id);
ret += getRadioTechnology(telephonyManager)
}
return ret
}
This is how I did it:
telephonyManager.listen(object : PhoneStateListener() {
override fun onServiceStateChanged(serviceState: ServiceState) {
val isNrAvailable = serviceState.toString().isNrAvailable()
// use isNrAvailable
}
}, PhoneStateListener.LISTEN_SERVICE_STATE)
Where
fun String.isNrAvailable() =
contains("nrState=CONNECTED") ||
contains("nsaState=5"))

How can a Delagetes.observable in a BroadcastReceiver be unit tested?

How can i test a Delegates.Observable that is inside a BroadcastReceiver. I need to get battery level of device and check if it's just went below or above pre-defined critical level, and upload to server using UseCase of clean architecture. I used observable to observe only changing states.
private fun handleIntent(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BATTERY_CHANGED -> {
try {
val batteryStatus =
context.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
batteryPct = (level / scale.toFloat() * 100).toInt()
isBatteryBelowCritical = batteryPct > CRITICAL_BATTERY
} catch (e: Exception) {
}
}
}
}
And observable
private var isBatteryBelowCritical by Delegates.observable(false) { _, old, new ->
//has gone above critical battery value
if (old && !new) {
sendAlarmUseCase.sendBatteryAlarm(batteryPct)
} else if (!old && new) {
// has gone below critical battery value
sendAlarmUseCase.sendBatteryAlarm(batteryPct)
}
}
Do i have to use parameters or assume old value to test current value? How is state is tested? Should i use parameterized test or assume previous value?
You could use a kind of dependency injection and refactor out the logic that checks for the state change:
fun notifyOnlyOnChange(initialValue: Boolean, notify: () -> Unit): ReadWriteProperty<Any?, Boolean> =
Delegates.observable(initialValue) { _, old, new ->
if (old != new) // your logic can be simplified to this
notify()
}
Then you can use it in your BroadcastReceiver like this:
private var isBatteryBelowCritical by notifyOnlyOnChange(false) {
sendAlarmUseCase.sendBatteryAlarm(batteryPct)
}
And unit test it like this:
#Test
fun `test observers are not notified when value is not changed`() {
var observable1 by notifyOnlyOnChange(false) { fail() }
observable1 = false
var observable2 by notifyOnlyOnChange(true) { fail() }
observable2 = true
}
#Test
fun `test observers are notified when value is changed`() {
var notified1 = false
var observable1 by notifyOnlyOnChange(false) { notified1 = true }
observable1 = true
assertTrue(notified1)
var notified2 = false
var observable2 by notifyOnlyOnChange(true) { notified2 = true }
observable2 = false
assertTrue(notified2)
}

Android WiFi Manager enableNetwork returns true but NetworkInfo state is always DISCONNECTED/SCANNING

I have written some code to enable a network with the given networkId, and to handle the asynchronous response via a BroadcastReceiver. However, even though enableNetwork returns true (indicating the OS successfully issue the command) my BroadcastReceiver never receives a NetworkInfo with CONNECTED state, it receives 2 events: DISCONNECTED and then DISCONNECTED/SCANNING.
From all the official docs and various SO questions I have read, if enableNetwork returns true then the BroadcastReceiver registered for handling NETWORK_STATE_CHANGED_ACTION intents should always receive a NetworkInfo object with state CONNECTED.
Here is the 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() {
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)
}
Can anyone help? I'm really stumped. The networkId I am passing is valid as it has been created from addNetwork which is succeeding as it's not returning -1.
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 code in my question wasn't completely correct, but, it also wasn't the root cause of my problem. 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 corresponded to the index of an element in a String array which was implicitly associated to the aforementioned BitSet 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 BitSet, passing in the correct index which would correspond to the element of the 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)
}

Categories

Resources