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)
Related
I have an Android Kotlin app where I am using ConnectivityManager.NetworkCallback.
The code looks like this (I have removed a few of the other functions for clarity):
val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
cm.registerDefaultNetworkCallback(object :NetworkCallback(FLAG_INCLUDE_LOCATION_INFO){
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
super.onCapabilitiesChanged(network, networkCapabilities)
val ssid =
if (Build.VERSION.SDK_INT >= 29) {
(networkCapabilities.transportInfo as WifiInfo).ssid
}
else {
(applicationContext?.getSystemService(Context.WIFI_SERVICE) as WifiManager).connectionInfo.ssid
}
Log.d(TAG, "onCapabilitiesChanged network: $network ssid: $ssid")
}
})
Problem is that the constructor NetworkCallback(FLAG_INCLUDE_LOCATION_INFO) only exists for Android API 31 and up.
For others, I need to use NetworkCallback().
This would mean I have to effectively write the code twice or create some second level to call. Maybe my own class that implements the methods.
Is there any way to do the inline override once?
Some way to choose which constructor to use and still do the override once?
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'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 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)
}
}