I am building an app where the connection to a 2nd device is the essence. Therefore, I used the WifiNetworkSpecifier API. However, the application must be able to automatically reconnect to the target network once the users leave and return to the Wi-Fi perimeter. Thus, I used the WifiNetworkSuggestion API. However, I am experiencing several issues there:
Once I get connected to the SSID using the specifier API and I confirm the push notification generated by the suggestion API, the suggestion API does not seem to work until I manually disconnect from the SSID (unregister network callback previously assigned to the specifier request) or kill the application.
If there is another network present in the perimeter which the user previously connected to by using the OS Wi-Fi manager (a hotspot, for instance), Android will prioritize this network, hence the suggestion API for my application would never auto-reconnect to the wanted and accessible SSID.
From my experience and understanding (which might be wrong) so far, it seems like we have to manually unregister the network callback previously assigned to the specifier request, or kill the application, and let the suggestion API to do its thing until it can work properly. This might be problematic if there are other networks (which the user previously connected to by using the OS Wi-Fi manager) present in the perimeter. In this case, we'd never auto-reconnect to the SSID defined by the application and the suggestion API would never work.
The question is: how to combine those two APIs to be able to connect to an SSID, yet auto-reconnect, without doing such ugly hacks as manually disconnecting the user, or killing the application, which also doesn't give us any guarantees?
In my opinion, this whole new implementation with the new network APIs is not done well, it's creating a lot of issues and restrictions for developers, or at least it's poorly documented.
Here's the code used for making the requests. Note that the device I'm connecting to does not have actual internet access, it's just used as a p2p network.
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun connectToWiFiOnQ(wifiCredentials: WifiCredentials, onUnavailable: () -> Unit) {
val request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(createWifiNetworkSpecifier(wifiCredentials))
.build()
networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
connectivityManager.bindProcessToNetwork(network)
}
override fun onUnavailable() {
super.onUnavailable()
onUnavailable.invoke()
}
}
networkCallback?.let {
addNetworkSuggestion(wifiCredentials)
connectivityManager.requestNetwork(request, it)
}
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun addNetworkSuggestion(wifiCredentials: WifiCredentials) {
wifiManager.addNetworkSuggestions(listOf(createWifiNetworkSuggestion(wifiCredentials))).apply {
if (this != WifiManager.STATUS_NETWORK_SUGGESTIONS_SUCCESS) {
if (this == WifiManager.STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP) {
wifiManager.removeNetworkSuggestions(emptyList())
addNetworkSuggestion(wifiCredentials)
}
}
}
suggestionBroadcastReceiver?.let { context.unregisterReceiver(it) }
suggestionBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)
return
// Post connection processing..
}
}
context.registerReceiver(
suggestionBroadcastReceiver, IntentFilter(WifiManager.ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION)
)
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun createWifiNetworkSpecifier(wifiCredentials: WifiCredentials): WifiNetworkSpecifier {
return when (wifiCredentials.authenticationType.toLowerCase()) {
WifiCipherType.NOPASS.name.toLowerCase() -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
WifiCipherType.WPA.name.toLowerCase() -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setWpa2Passphrase(wifiCredentials.password)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
else -> WifiNetworkSpecifier.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
}
}
#RequiresApi(api = Build.VERSION_CODES.Q)
private fun createWifiNetworkSuggestion(wifiCredentials: WifiCredentials): WifiNetworkSuggestion {
return when (wifiCredentials.authenticationType.toLowerCase()) {
WifiCipherType.NOPASS.name.toLowerCase() -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
WifiCipherType.WPA.name.toLowerCase() -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setWpa2Passphrase(wifiCredentials.password)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
else -> WifiNetworkSuggestion.Builder()
.setSsid(wifiCredentials.networkSSID)
.setIsHiddenSsid(wifiCredentials.isSSIDHidden)
.build()
}
}
Calling the suggestion API in onAvailable works for me. That way the user doesn't see two popups at the same time either.
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
connectivityManager.bindProcessToNetwork(network)
addNetworkSuggestion(wifiCredentials)
}
}
Related
I have NetworkUtils to monitor the connection state:
object NetworkUtils {
lateinit var connectivityManager: ConnectivityManager
var isConnected = false; private set
private object NetworkCallback : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
isConnected = true
}
override fun onLost(network: Network) {
isConnected = false
}
}
fun init(context: Context) {
connectivityManager = context.getSystemService(ConnectivityManager::class.java)
}
fun isConnectedDeprecated(): Boolean {
val networkInfo = connectivityManager.activeNetworkInfo
return networkInfo?.isConnected == true
}
fun registerNetworkCallback() = connectivityManager.registerDefaultNetworkCallback(NetworkCallback)
fun unregisterNetworkCallback() = connectivityManager.unregisterNetworkCallback(NetworkCallback)
}
And Interceptor I use with Retrofit:
class MyInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: IOException) {
throw if (NetworkUtils.isConnected()) {
ExceptionA()
} else {
ExceptionB()
}
}
}
}
The point is to know if IOException thrown from request caused by no connection (ExceptionB) or if it's some other network issue (ExceptionA).
The issue is if I turn off WIFI on my device in the middle of the request I expect to get ExceptionB, but sometimes I get ExceptionA. Because when interceptor catches IOException NetworkCallback's onLost isn't called yet.
I suspect that's because By default, the callback methods are called on the connectivity thread of your app, which is a separate thread used by ConnectivityManager. (link)
And Retorfit runs interceptors on a different thread. So there's no any guaranteed order.
So is there a way to be sure that NetworkCallback will be hit before interceptor will catch the exception?
I know we can pass in Handler when registering the NetworkCallback, and maybe that could help us to somehow run NetworkCallback on the same thread as Retrofit interceptors. But I have no idea how to do it and it looks like a bit dirty solution.
Also, if check NetworkUtils.isConnectedDeprecated() in interceptor instead of NetworkUtils.isConnected then it works exactly like I want to. But documentation says:
Deprecated. Apps should instead use the ConnectivityManager.NetworkCallback API to learn about connectivity changes. These will give a more accurate picture of the connectivity state of the device and let apps react more easily and quickly to changes.
So it's not more quickly if NetworkCallback is called with some delay, huh?
In my Flutter app, I'm using native Android code (in Kotlin) to connect to a specific Wi-Fi programmatically. For Android 10+, I need to use network specifier. It works, but only if an app is in foreground. In background, I always end up in onUnavailable callback.
In my specific use case, the app connects to a highly unstable Wi-Fi (it turns off and on all time). I need the app to be able to reconnect without any user interaction even with the display turned off.
Is there a way to connect to a specific network in background?
val specifier = WifiNetworkSpecifier.Builder()
.setSsid(ssid)
.setBssid(MacAddress.fromString(macAddressString))
.apply {
if (isWpa3 != null && isWpa3) {
setWpa3Passphrase(password)
} else {
setWpa2Passphrase(password)
}
}
.build()
var request = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(specifier)
.build()
this.networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
connectivityManager.bindProcessToNetwork(network)
// Success
}
override fun onUnavailable() {
super.onUnavailable()
// Unavailable
// Always end up here with display off
}
}
// Defined elsewhere
connectivityManager.requestNetwork(request, networkCallback)
I am using BluetoothLeScanner to scan for BLE devices and get a list of objects representing the devices to show inside my app (not connecting to any of them).
I am interested in doing the same, but using the CompanionDeviceManager now. Its callback CompanionDeviceManager.Callback.onDeviceFound(chooserLauncher: IntentSender?) unfortunately does not return any human readable form of found devices... the closest it gets is the IntentSender.writeToParcel method, but I am not sure how to use it in this situation.
I am not constrained to use the CompanionDeviceManager but I wanted to follow the OS version specific guidelines, we are supposed to use CompanionDeviceManager for Bluetooth devices scanning starting from API 26, but it seems useless in my case... so is there any way to get devices data from that callback, or should I just ditch it and stay with BluetoothLeScanner for all OS versions?
Late answer but it might help someone else. You can create a bluetooth device picker in combination with ActivityResultContracts.StartIntentSenderForResult() in order to get the BluetoothDevice. From there you will have access to all the device info that you need. Recent changes added some Android 12 permissions like android.permission.BLUETOOTH_CONNECT. Your mileage may vary.
val context = LocalContext.current
// Get the device manager instance
val deviceManager: CompanionDeviceManager by lazy {
ContextCompat.getSystemService(
context,
CompanionDeviceManager::class.java
) as CompanionDeviceManager
}
// Create a filter of your choice. Here I just look for specific device names
val deviceFilter: BluetoothDeviceFilter by lazy {
BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile(supportedDevices))
.build()
}
// Create a pairing request with your filter from the last step
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
// Create a picker for discovered bluetooth devices
val bluetoothDevicePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
val device: BluetoothDevice? =
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
try {
// Now that you have the desired device, do what you need to with it
device?.apply {
when {
name?.matches(Regex(firstDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onFirstDeviceDiscovered(device)
}
name?.matches(Regex(secondDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onSecondDeviceDiscovered(device)
}
}
}
} catch (e: SecurityException) {
e.printStackTrace()
//TODO: handle the security exception (this is possibly a bug)
// https://issuetracker.google.com/issues/198986283
}
}
)
// A utility function to centralize calling associate (optional)
val associateDevice: (AssociationRequest) -> Unit = { request ->
// Attempt to associate device(s)
deviceManager.associate(
request,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
val sender = IntentSenderRequest.Builder(chooserLauncher)
.build()
bluetoothDevicePicker.launch(sender)
}
override fun onFailure(error: CharSequence?) {
//TODO: handle association failure
}
}, null
)
}
I'm trying to connect to a Wi-Fi hotspot on Android 10 which has no available Internet connection. The problem is that as soon as I'm connected and the device detects there's no connection, it goes back to another connection with Internet available.
After looking up what I can do I've found this piece of code which supposedly forces staying:
val manager = applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
val mWifiLock: WifiManager.WifiLock = manager.createWifiLock(WifiManager.WIFI_MODE_FULL, "wifiLock")
if (! mWifiLock.isHeld) {
mWifiLock.acquire()
}
Unfortunately, that code is deprecated and doesn't work anymore. So far, the only working solutions I've found was disabling the other networks. I've also found other threads on StackOverflow with the exact same problem, but those were created 1+ years ago so I figured I might ask this again.
Does anyone know of an updated way I can stay connected to this network?
This is the code I use for connecting to the hotspot btw:
val wifiNetworkSpecifier = WifiNetworkSpecifier.Builder()
.setSsid(ssid)
.setWpa2Passphrase(pass)
.build()
val wifiRequest = NetworkRequest.Builder()
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
.removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.setNetworkSpecifier(wifiNetworkSpecifier)
.build()
val connectivity = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?
val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onUnavailable() {
super.onUnavailable()
}
override fun onLosing(network: Network, maxMsToLive: Int) {
super.onLosing(network, maxMsToLive)
listener(false)
}
override fun onLost(network: Network) {
super.onLost(network)
listener(false)
}
override fun onAvailable(network: Network) {
super.onAvailable(network)
connectivity?.bindProcessToNetwork(network)
//Code to force-stay
listener(true)
}
}
connectivity?.requestNetwork(wifiRequest, networkCallback)
I'm trying to make a SMB (Samba) connection to get a list of files and download them with the SMBClient of smbj library.
To that I have to connect to a specific network and use that class, but in Android Q I have to change the way to connect to the wireless, like this:
val wifiNetworkSpecifier: WifiNetworkSpecifier = WifiNetworkSpecifier.Builder().apply {
setSsid(ssid)
setWpa2Passphrase(password)
}.build()
val networkRequest: NetworkRequest = NetworkRequest.Builder().apply {
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
setNetworkSpecifier(wifiNetworkSpecifier)
}.build()
val networkCallback: ConnectivityManager.NetworkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
Log.d(tag, "::onAvailable - Entry")
super.onAvailable(network)
}
override fun onUnavailable() {
Log.d(tag, "::onUnavailable - Entry")
super.onUnavailable()
}
}
This makes a connection in the app, but establishes the main connection via mobile data and I can't establish a connection because the server is unreachable. I have to find a way to make the connection through the network object in the onAvailable function.
Did you know how or is there an alternative way?
Solution
I found a method in the ConnectivityManager class the method is bindProcessToNetwork
connectivityManager.bindProcessToNetwork(network)
I found a method in the ConnectivityManager class the method is bindProcessToNetwork
connectivityManager.bindProcessToNetwork(network)