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
configuredNetwork.SSID.unescape() == ssid && configuredNetwork.BSSID == bssid
toconfiguredNetwork.SSID.unescape() == ssid
---- Side Note #2 I noticed that you do not listen forSupplicantState.ASSOCIATING
. True connection/disconnection exists only when you are no longer associating. – NorvinWifiConfig
object from theWifiManager.configuredNetworks
after callingaddNetwork
theBSSID
was00:00:00:00:00:00:00
which I think indicates it was not saved. – Emmet